@clearlysh/beehaven 0.8.30 → 0.8.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,9 +53,9 @@ export const PUBLIC_BUILD = true;
53
53
  * ⚠ REWRITTEN IN `dist/` AT BUILD TIME, like `PUBLIC_BUILD`. `unstamped` is what a source checkout
54
54
  * carries and what `tsx`/`vitest` see, and it is honest: nothing has stamped them.
55
55
  */
56
- export const BUILD_SHA = '2c7d78e7c945';
56
+ export const BUILD_SHA = '3767a788c0ee';
57
57
  /** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
58
- export const BUILD_TIME = '2026-10-13T06:00Z';
58
+ export const BUILD_TIME = '2026-10-13T06:40Z';
59
59
  /**
60
60
  * The build identity as one short string, for `--version`.
61
61
  *
@@ -43,9 +43,10 @@ import { execFileSync } from 'node:child_process';
43
43
  import { createHash } from 'node:crypto';
44
44
  import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
45
45
  import { homedir, tmpdir } from 'node:os';
46
- import { join, sep } from 'node:path';
46
+ import { dirname, join, sep } from 'node:path';
47
47
  import { fileURLToPath } from 'node:url';
48
- import { usableUpdateDigest } from '../version-utils.js';
48
+ import { compareVersions, usableUpdateDigest } from '../version-utils.js';
49
+ import { npmUpdatePlan, latestNpmVersion, installedNpmVersion, installNpmVersionSync } from '../npm-update.js';
49
50
  /**
50
51
  * Which install this process is.
51
52
  *
@@ -83,8 +84,9 @@ export function updateHint(kind = installKind()) {
83
84
  return 'Run \x1b[1mbeehaven update\x1b[0m';
84
85
  if (kind === 'npx')
85
86
  return 'Run `npx @clearlysh/beehaven@latest` for the current npm release.';
87
+ // npm installs update themselves since CAN2-57 (the daemon hourly, `beehaven update` on demand).
86
88
  if (kind === 'npm')
87
- return 'Run `npm install -g @clearlysh/beehaven@latest` to update this npm install.';
89
+ return 'Run \x1b[1mbeehaven update\x1b[0m (or `npm install -g @clearlysh/beehaven@latest`).';
88
90
  return 'This is a linked dev build — `npm run build` in beehaven_sdk/packages/cli.';
89
91
  }
90
92
  /**
@@ -113,9 +115,11 @@ export function runSelfUpdate(kind = installKind()) {
113
115
  if (kind === 'linked') {
114
116
  return { ok: false, message: 'This is a linked development build — refusing to overwrite it.\n cd beehaven_sdk/packages/cli && npm run build' };
115
117
  }
116
- if (kind === 'npm' || kind === 'npx') {
118
+ if (kind === 'npx') {
117
119
  return { ok: false, message: updateHint(kind) };
118
120
  }
121
+ if (kind === 'npm')
122
+ return runNpmSelfUpdate();
119
123
  const feed = (process.env.BEEHAVEN_CLI_FEED || 'https://downloads.clearly.sh/cli/').replace(/\/+$/, '');
120
124
  const installerUrl = process.env.BEEHAVEN_INSTALL_URL || 'https://www.clearly.sh/install.sh';
121
125
  const temp = mkdtempSync(join(tmpdir(), 'beehaven-self-update-'));
@@ -154,9 +158,32 @@ export function runSelfUpdate(kind = installKind()) {
154
158
  rmSync(temp, { recursive: true, force: true });
155
159
  }
156
160
  }
161
+ /**
162
+ * ⚠ npm installs (the only Windows install) update with npm itself — see `npm-update.ts` for why
163
+ * it refuses rather than guessing when this copy is not npm's global install or the prefix is not
164
+ * writable. The daemon restarts itself after ITS hourly update; after this manual one the running
165
+ * daemon keeps the old code until restarted, so the message says so.
166
+ */
167
+ function runNpmSelfUpdate() {
168
+ const manual = 'Update it by hand: npm install -g @clearlysh/beehaven@latest';
169
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
170
+ const plan = npmUpdatePlan(pkgRoot);
171
+ if (!plan.ok)
172
+ return { ok: false, message: `Can't update this npm install here: ${plan.reason}.\n ${manual}` };
173
+ const latest = latestNpmVersion(plan.npm);
174
+ if (!latest)
175
+ return { ok: false, message: `Update failed: could not read the latest version from npm.\n ${manual}` };
176
+ const current = installedNpmVersion(plan.globalRoot) ?? '0.0.0';
177
+ if (compareVersions(latest, current) <= 0)
178
+ return { ok: true, message: `Already up to date (${current}).` };
179
+ const r = installNpmVersionSync(plan, latest);
180
+ return r.ok
181
+ ? { ok: true, message: `${r.message} A daemon that is already running keeps the old version until: beehaven restart` }
182
+ : { ok: false, message: `${r.message}\n ${manual}` };
183
+ }
157
184
  /** True when this install can update itself at all — what the notifier uses to pick its wording. */
158
185
  export function canSelfUpdate(kind = installKind()) {
159
- return kind === 'user';
186
+ return kind === 'user' || kind === 'npm';
160
187
  }
161
188
  /**
162
189
  * Is a bundled CLI present that the shell can NEVER reach, whatever its version?
package/dist/daemon.js CHANGED
@@ -46,6 +46,7 @@ import { homedir, tmpdir } from 'os';
46
46
  import { join, dirname, relative } from 'path';
47
47
  import { fileURLToPath } from 'url';
48
48
  import { compareVersions, usableUpdateDigest } from './version-utils.js';
49
+ import { npmUpdatePlan, installNpmVersion } from './npm-update.js';
49
50
  import { installKind } from './commands/self-update.js';
50
51
  const __dirname = dirname(fileURLToPath(import.meta.url));
51
52
  const CONFIG_DIR = join(homedir(), '.beehaven');
@@ -963,9 +964,85 @@ function updatesAreManagedExternally() {
963
964
  *
964
965
  * ⚠ NOTHING ELSE CALLS IT. The daemon's timer is still the only production caller.
965
966
  */
967
+ /** Said once per daemon, not hourly — see the managed-updates note below. */
968
+ let announcedNpmUpdateBlocked = false;
969
+ /**
970
+ * Start the freshly installed version and leave (shared by the curl and npm update paths).
971
+ * A CLI-spawned headless daemon is detached, with no supervisor to interpret exit 75, so it starts
972
+ * the new entry in a helper that waits until this process releases the IPC socket.
973
+ */
974
+ async function restartForUpdate() {
975
+ if (process.env.BEEHAVEN_CLI_OWNED_DAEMON === '1') {
976
+ const child = spawn(process.execPath, [join(__dirname, 'daemon-entry.js'), '--restart-after-update', String(process.pid)], {
977
+ detached: true,
978
+ stdio: 'ignore',
979
+ env: process.env,
980
+ });
981
+ child.on('error', (error) => console.error(` [update] Restart helper failed: ${error.message}`));
982
+ if (!child.pid)
983
+ throw new Error('could not start update restart helper');
984
+ child.unref();
985
+ }
986
+ if (daemonRelay?.isConnected())
987
+ await new Promise((r) => setTimeout(r, 500));
988
+ process.exit(75);
989
+ }
990
+ /**
991
+ * ⚠⚠ npm INSTALLS (THE ONLY WINDOWS INSTALL) NEVER AUTO-UPDATED (CAN2-57). This routine re-ran the
992
+ * bash installer, so it skipped every npm install and a Windows user stayed on their first version.
993
+ * They now update with npm itself — to the version the release manifest names (stable only) — when
994
+ * this copy IS npm's global install and its folder is writable; otherwise one line says why and
995
+ * what to run. No rollback file: `install.sh` cannot reinstall an npm package; a bad release is
996
+ * undone with `npm install -g @clearlysh/beehaven@<previous>`.
997
+ */
998
+ async function checkAndAutoUpdateNpm() {
999
+ // A headless daemon's stdout goes nowhere, so the update story also goes to daemon.log.
1000
+ const say = (m, level = 'info') => {
1001
+ (level === 'error' ? console.error : console.log)(` [update] ${m}`);
1002
+ try {
1003
+ logger[level]('daemon', `[update] ${m}`);
1004
+ }
1005
+ catch { /* logging never blocks an update */ }
1006
+ };
1007
+ try {
1008
+ const controller = new AbortController();
1009
+ const timeout = setTimeout(() => controller.abort(), 5000);
1010
+ const res = await fetch(LATEST_MANIFEST, { signal: controller.signal });
1011
+ clearTimeout(timeout);
1012
+ if (!res.ok)
1013
+ return;
1014
+ const latest = (await res.json())?.version;
1015
+ if (typeof latest !== 'string')
1016
+ return;
1017
+ const current = getCurrentVersion();
1018
+ if (compareVersions(latest, current) <= 0)
1019
+ return;
1020
+ const plan = npmUpdatePlan(join(__dirname, '..'));
1021
+ if (!plan.ok) {
1022
+ if (!announcedNpmUpdateBlocked) {
1023
+ announcedNpmUpdateBlocked = true;
1024
+ say(`${latest} is available, but this npm install cannot update itself: ${plan.reason}. Run: npm install -g @clearlysh/beehaven@latest`);
1025
+ }
1026
+ return;
1027
+ }
1028
+ say(`Updating ${current} → ${latest} with npm...`);
1029
+ const r = await installNpmVersion(plan, latest);
1030
+ if (!r.ok)
1031
+ throw new Error(r.message);
1032
+ if (getCurrentVersion() !== latest)
1033
+ throw new Error(`npm did not install ${latest}`);
1034
+ say(`Updated to v${latest} — restarting...`);
1035
+ await restartForUpdate();
1036
+ }
1037
+ catch (error) {
1038
+ say(`Update failed: ${error?.message ?? error}`, 'error');
1039
+ }
1040
+ }
966
1041
  export async function checkAndAutoUpdate() {
967
1042
  const owner = updatesAreManagedExternally();
968
1043
  const runningKind = installKind(join(__dirname, 'daemon-entry.js'));
1044
+ if (!owner && runningKind === 'npm')
1045
+ return checkAndAutoUpdateNpm();
969
1046
  if (owner || runningKind !== 'user') {
970
1047
  // Said once at startup, not hourly — a line every hour about a thing that is working as
971
1048
  // designed is how a log stops being read.
@@ -1017,19 +1094,6 @@ export async function checkAndAutoUpdate() {
1017
1094
  throw new Error(`installer did not install ${latest}`);
1018
1095
  }
1019
1096
  console.log(` [update] Updated to v${latest} — restarting...`);
1020
- if (process.env.BEEHAVEN_CLI_OWNED_DAEMON === '1') {
1021
- // A CLI-spawned headless daemon is detached, with no supervisor to interpret exit 75.
1022
- // Start the new entry in a helper that waits until this process releases the IPC socket.
1023
- const child = spawn(process.execPath, [join(__dirname, 'daemon-entry.js'), '--restart-after-update', String(process.pid)], {
1024
- detached: true,
1025
- stdio: 'ignore',
1026
- env: process.env,
1027
- });
1028
- child.on('error', (error) => console.error(` [update] Restart helper failed: ${error.message}`));
1029
- if (!child.pid)
1030
- throw new Error('could not start update restart helper');
1031
- child.unref();
1032
- }
1033
1097
  // Drop the temp tarball BEFORE exiting. `process.exit` skips `finally`,
1034
1098
  // so cleaning up only there would leak the whole release into /tmp on every
1035
1099
  // SUCCESSFUL update — the one path that always runs.
@@ -1046,13 +1110,9 @@ export async function checkAndAutoUpdate() {
1046
1110
  catch { }
1047
1111
  installerPath = null;
1048
1112
  }
1049
- // Give relay a moment to flush
1050
- if (daemonRelay?.isConnected()) {
1051
- await new Promise((r) => setTimeout(r, 500));
1052
- }
1053
- // Exit with non-zero so launchd/systemd restarts with the new version
1054
- // (KeepAlive > SuccessfulExit > false means exit 0 stays down)
1055
- process.exit(75);
1113
+ // Start the new version and exit non-zero, so launchd/systemd also restarts with it
1114
+ // (KeepAlive > SuccessfulExit > false means exit 0 stays down).
1115
+ await restartForUpdate();
1056
1116
  }
1057
1117
  catch (error) {
1058
1118
  console.error(` [update] Update failed: ${error?.message ?? error}`);
@@ -0,0 +1,138 @@
1
+ /**
2
+ * npm-update — updating a CLI that was installed with `npm install -g` (CAN2-57).
3
+ *
4
+ * ⚠⚠ npm IS THE ONLY WINDOWS INSTALL, AND IT NEVER UPDATED. The daemon's hourly updater re-runs the
5
+ * bash `install.sh`, so it served only `~/.beehaven/cli` installs; `beehaven update` refused npm
6
+ * installs and printed the npm command instead. A Windows user stayed on whatever version they
7
+ * first installed until they noticed the notice. Both paths now call this module.
8
+ *
9
+ * ⚠ IT UPDATES THE COPY THAT IS RUNNING, OR NOTHING. The npm that sits beside the running Node is
10
+ * asked for its global root; if this package is not the one installed there (an npx run, a second
11
+ * Node, a copied folder), installing would create a SECOND copy that shadows or is shadowed by
12
+ * this one — the failure `self-update.ts` documents for curl installs. So it refuses and says why.
13
+ *
14
+ * ⚠ A GLOBAL PREFIX THE USER CANNOT WRITE IS NOT "TRY sudo". Windows (`%AppData%\npm`) and
15
+ * Homebrew/nvm prefixes are user-writable; a system `/usr/local` usually is not, and an unattended
16
+ * daemon must never escalate. Not writable → the one-line manual command, nothing else.
17
+ *
18
+ * ⚠ THE EXACT VERSION, NOT `@latest`. The caller passes the version from the release manifest
19
+ * (`latest.json`, stable only), so the channel is decided by our publish script, never by a dist
20
+ * tag someone moved by hand.
21
+ */
22
+ import { execFile, execFileSync } from 'child_process';
23
+ import { accessSync, constants, existsSync, readFileSync, realpathSync } from 'fs';
24
+ import { homedir } from 'os';
25
+ import { dirname, join, resolve } from 'path';
26
+ export const PACKAGE = '@clearlysh/beehaven';
27
+ const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
28
+ /** The npm that belongs to the Node running this process, else whatever `npm` is on PATH. */
29
+ export function npmCommand(platform = process.platform, execPath = process.execPath) {
30
+ const local = join(dirname(execPath), platform === 'win32' ? 'npm.cmd' : 'npm');
31
+ return existsSync(local) ? local : platform === 'win32' ? 'npm.cmd' : 'npm';
32
+ }
33
+ /** Windows `.cmd` shims cannot be spawned without a shell (Node refuses with EINVAL since 18.20/20.12). */
34
+ const needsShell = (cmd) => /\.cmd$/i.test(cmd);
35
+ const quoteForShell = (s) => (/[\s"&|<>^]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s);
36
+ /** Run npm synchronously; returns stdout. Never inside the package directory (Windows rename locks). */
37
+ function npmSync(npm, args, timeoutMs) {
38
+ const shell = needsShell(npm);
39
+ return execFileSync(shell ? quoteForShell(npm) : npm, args, {
40
+ encoding: 'utf8',
41
+ cwd: homedir(),
42
+ timeout: timeoutMs,
43
+ stdio: ['ignore', 'pipe', 'pipe'],
44
+ shell,
45
+ windowsHide: true,
46
+ });
47
+ }
48
+ /**
49
+ * Can this install update itself with npm? `pkgRoot` is the running package's root (the folder
50
+ * holding its package.json).
51
+ */
52
+ export function npmUpdatePlan(pkgRoot, platform = process.platform) {
53
+ const npm = npmCommand(platform);
54
+ let globalRoot;
55
+ try {
56
+ globalRoot = npmSync(npm, ['root', '-g'], 30_000).trim();
57
+ }
58
+ catch (e) {
59
+ return { ok: false, npm, reason: `npm is not available (${String(e?.code ?? e?.message ?? e).slice(0, 80)})` };
60
+ }
61
+ const real = (p) => { try {
62
+ return realpathSync(p);
63
+ }
64
+ catch {
65
+ return resolve(p);
66
+ } };
67
+ const installed = join(globalRoot, ...PACKAGE.split('/'));
68
+ if (real(installed).toLowerCase() !== real(pkgRoot).toLowerCase()) {
69
+ return { ok: false, npm, globalRoot, reason: `this copy (${pkgRoot}) is not npm's global install (${installed})` };
70
+ }
71
+ try {
72
+ accessSync(globalRoot, constants.W_OK);
73
+ accessSync(installed, constants.W_OK);
74
+ }
75
+ catch {
76
+ return { ok: false, npm, globalRoot, reason: `npm's global folder is not writable by you (${globalRoot})` };
77
+ }
78
+ return { ok: true, npm, globalRoot };
79
+ }
80
+ /** The version npm's global install of this package reports, read from disk. */
81
+ export function installedNpmVersion(globalRoot) {
82
+ try {
83
+ return JSON.parse(readFileSync(join(globalRoot, ...PACKAGE.split('/'), 'package.json'), 'utf8')).version ?? null;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ /** The newest published version, from npm (for `beehaven update`; the daemon uses the release manifest). */
90
+ export function latestNpmVersion(npm) {
91
+ try {
92
+ const v = npmSync(npm, ['view', PACKAGE, 'version', '--registry=https://registry.npmjs.org'], 30_000).trim();
93
+ return VERSION_RE.test(v) ? v : null;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ /** The arguments that install exactly `version` globally — exported so the shape is pinned. */
100
+ export function npmInstallArgs(version) {
101
+ if (!VERSION_RE.test(version))
102
+ throw new Error(`not a release version: ${version}`);
103
+ return ['install', '-g', `${PACKAGE}@${version}`, '--no-audit', '--no-fund', '--registry=https://registry.npmjs.org'];
104
+ }
105
+ /** Install `version` (blocking — the CLI's `update`). Verifies what landed on disk. */
106
+ export function installNpmVersionSync(plan, version) {
107
+ if (!plan.ok || !plan.globalRoot)
108
+ return { ok: false, message: plan.reason ?? 'npm update not possible here' };
109
+ try {
110
+ npmSync(plan.npm, npmInstallArgs(version), 5 * 60_000);
111
+ }
112
+ catch (e) {
113
+ const detail = String(e?.stderr || e?.message || e).trim().split('\n').slice(-3).join(' ').slice(0, 240);
114
+ return { ok: false, message: `npm install failed: ${detail}` };
115
+ }
116
+ const got = installedNpmVersion(plan.globalRoot);
117
+ return got === version
118
+ ? { ok: true, message: `Updated to ${version}.` }
119
+ : { ok: false, message: `npm reported success but ${got ?? 'nothing'} is installed, not ${version}.` };
120
+ }
121
+ /** Install `version` without blocking the event loop (the daemon's hourly updater). */
122
+ export function installNpmVersion(plan, version) {
123
+ return new Promise((resolveP) => {
124
+ if (!plan.ok || !plan.globalRoot)
125
+ return resolveP({ ok: false, message: plan.reason ?? 'npm update not possible here' });
126
+ const shell = needsShell(plan.npm);
127
+ execFile(shell ? quoteForShell(plan.npm) : plan.npm, npmInstallArgs(version), {
128
+ cwd: homedir(), timeout: 5 * 60_000, shell, windowsHide: true,
129
+ }, (err, _out, stderr) => {
130
+ if (err)
131
+ return resolveP({ ok: false, message: `npm install failed: ${String(stderr || err.message).trim().split('\n').slice(-3).join(' ').slice(0, 240)}` });
132
+ const got = installedNpmVersion(plan.globalRoot);
133
+ resolveP(got === version
134
+ ? { ok: true, message: `Updated to ${version}.` }
135
+ : { ok: false, message: `npm reported success but ${got ?? 'nothing'} is installed, not ${version}.` });
136
+ });
137
+ });
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clearlysh/beehaven",
3
- "version": "0.8.30",
3
+ "version": "0.8.31",
4
4
  "description": "Beehaven CLI — command your Clearly workspace, agents, and canvas from the terminal",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,6 +40,12 @@ const SOURCES = {
40
40
  'codebase-map': 'apps/mcp-server/plugin/skills/codebase-map/SKILL.md',
41
41
  'clearly-workspace': 'apps/mcp-server/plugin/skills/clearly-workspace/SKILL.md',
42
42
  'clearly-agent': 'apps/mcp-server/plugin/skills/clearly-agent/SKILL.md',
43
+ // CAN2-57 — the rest of the plugin's skills: one set across plugin, CLI and Mac app.
44
+ 'visualize': 'apps/mcp-server/plugin/skills/visualize/SKILL.md',
45
+ 'sticker-pack': 'apps/mcp-server/plugin/skills/sticker-pack/SKILL.md',
46
+ 'clearly-docs': 'apps/mcp-server/plugin/skills/clearly-docs/SKILL.md',
47
+ 'clearly-init': 'apps/mcp-server/plugin/skills/clearly-init/SKILL.md',
48
+ 'clearly-workflows': 'apps/mcp-server/plugin/skills/clearly-workflows/SKILL.md',
43
49
  };
44
50
 
45
51
  let missing = 0;
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: clearly-docs
3
+ description: >-
4
+ Read, search, edit and recover a company's documents in a Clearly workspace.
5
+ Use the seven-tool MCP filesystem for ordinary work; use the `beehaven` CLI
6
+ only for document history, diffs, and restore. Load this whenever the answer
7
+ depends on what the company has written down, or when you are about to write
8
+ something down for it. Triggers: "what do our docs say", "find the doc about
9
+ X", "update the spec", "write this up", "add it to the docs", "has this
10
+ changed", "what did they edit", "search the wiki", "company knowledge", "our
11
+ documentation", "the PRD", "the runbook".
12
+ ---
13
+
14
+ > **Tool names below are written UNPREFIXED** (`clearly_read`). Your runtime may expose them
15
+ > with a server prefix — e.g. `mcp__plugin_clearly_clearly__clearly_read`. **Match by suffix,
16
+ > not by exact name**: a skill written against the bare name resolves to nothing otherwise, and
17
+ > the failure looks like "the tool doesn't exist" rather than "the name is decorated".
18
+ >
19
+ > **If no such tool is callable at all**, the Clearly MCP server isn't authorised in this
20
+ > session — these skills still LIST when it isn't, so you find out by firing a dead call.
21
+ > Authorise it (`/mcp`, or `claude mcp`).
22
+
23
+ ## What this is
24
+
25
+ The workspace is a **filesystem**, not a chat surface with a search box.
26
+
27
+ ```
28
+ ~/ every document
29
+ ~/clearly.md what is settled for the whole workspace
30
+ ~/<Folder>/ one directory per project
31
+ ~/<Folder>/clearly.md what is settled for that folder
32
+ ~/.archive/ put away — hidden, readable, read-only, still searched
33
+ ```
34
+
35
+ Folders are projects. Documents are `.md` files named `<KEY> <Title>.md` — e.g.
36
+ `CLR-42 Pricing.md`. The key is an address: `cat ~/CLR-42.md` works from anywhere, and so does
37
+ the full name and the bare title.
38
+
39
+ ## The loop
40
+
41
+ **Find it, then open it.** `clearly_glob` answers "what is it called", `clearly_grep` answers
42
+ "where is this mentioned", and `clearly_read` opens what either one returned.
43
+
44
+ ```
45
+ clearly_glob { pattern: "~/**/*.md" }
46
+ clearly_grep { pattern: "pricing", limit: 5 }
47
+ clearly_read { target: "~/Prepress/CLR-42 Pricing.md" }
48
+ ```
49
+
50
+ ⚠ **`clearly_grep` takes a REGEX, unparsed** — `|`, `(`, `$` and quotes mean what they mean in a
51
+ regular expression. Nothing shells out, so nothing eats them as syntax first.
52
+
53
+ ⚠ **Archived documents are excluded from both.** When any matched, the result carries
54
+ `archivedHidden` saying how many — mention that rather than reporting a clean miss. Search the
55
+ archive by naming it: `clearly_grep { pattern: …, path: "~/.archive" }`.
56
+
57
+ **Structural questions — a date range, a null body, counts — have no keyword to search for**, so
58
+ they go to SQL over the workspace's own tables. That is a CLI job, not an MCP tool:
59
+
60
+ ```
61
+ beehaven call bash '{"command":"sql SELECT title, updated_at FROM notes WHERE project_id IS NULL LIMIT 20"}'
62
+ ```
63
+
64
+ Read-only, one statement, credential columns redacted.
65
+
66
+ ## ⚠⚠ Read before you write, and check what changed
67
+
68
+ This is the part that makes documentation stay true, and it is the part agents skip.
69
+
70
+ The MCP read/edit/replace tools cover the current body. Document history is a separate CLI surface:
71
+
72
+ - **`beehaven call document-status '{"limit":20}'`** lists recent changes. `mine: true` is the
73
+ certain signal that the active agent made the latest change. `by: "unknown"` means the log has
74
+ no author; it may be a browser edit or an older unstamped agent write, so do not claim a person
75
+ made it.
76
+ - **`beehaven call document-diff '{"id":"<documentId>"}'`** follows up with added and removed
77
+ lines. Pass a `versionId` from status to compare against a specific checkpoint.
78
+
79
+ Run status when you resume a conversation, and whenever they say something implying the document
80
+ has moved — *"I rewrote that"*, *"take another look"*.
81
+
82
+ ⚠ **Their edit wins, always.** If a document now disagrees with something you concluded
83
+ earlier, your conclusion is the stale one. You are keeping their documents, not defending your
84
+ own record. Never quietly restore what they took out.
85
+
86
+ ## Editing
87
+
88
+ **`clearly_edit` (find/replace) is the default. `clearly_replace` REPLACES the whole document.**
89
+
90
+ To change one line of a 12,000-character document, `replace` makes you reproduce all 12,000 — and
91
+ anything you fail to retype is gone with no error. It is also a lost-update machine: it writes
92
+ whatever you last read, overwriting anything the human changed in between.
93
+
94
+ - `clearly_edit` requires the `find` text to match **exactly once**. Ambiguity is REFUSED with a
95
+ count, never guessed — an agent that edits the first of three matches has changed something
96
+ nobody looked at.
97
+ - Several independent document edits can be sent in one tool call with `batch`, but each batch
98
+ row is still one exact `{ target, find, replace, all? }` operation. There is no `multi_edit`
99
+ field and the batch is not transactional; inspect the per-item results and retry only failures.
100
+ - `clearly_replace` does not accept `expected_content` or a commit-message field. If you must replace
101
+ a whole existing document, re-read it immediately before writing and verify the returned body.
102
+ Prefer `clearly_edit` whenever a precise passage change can express the job.
103
+
104
+ ## Comments and @mentions — say it beside the work, not instead of it
105
+
106
+ A comment is ABOUT something; an edit changes it. When you have a question or a note for a person,
107
+ leave it where they will see it, on the thing itself:
108
+
109
+ ```
110
+ clearly_comment { target: "CLR-42", content: "Is this still the Q3 number? @sam" }
111
+ clearly_comment { target: "<sheetId>", content: "Double-counts refunds", fields: { cell: "Plan!D12" } }
112
+ clearly_comment { target: "<deckId>", content: "Tighten this headline", fields: { slide: 3 } }
113
+ clearly_read { type: "comment", target: "SS-41" } # the thread, on any item
114
+ clearly_read { type: "comment", target: "@me" } # where YOU were @mentioned
115
+ clearly_edit { type: "comment", target: "<mentionId>", changes: { acknowledged: true } }
116
+ ```
117
+
118
+ `target` is the item — a document, sheet, deck, ticket, board, thought or canvas, by id, key or exact
119
+ title. Anchor with `fields.quote` (document), `slide` (deck, required), `cell` (sheet), `column`
120
+ (board) or `pinId` / `x`,`y` (canvas); an anchor the item cannot carry is refused, never dropped.
121
+
122
+ `@handle` mentions a person (notified) or an agent (queued in its inbox). Get real handles with
123
+ `clearly_read { type: "comment", target, includeMentionables: true }`. The write's result lists
124
+ `mentions`, and `unmatchedMentions` for any handle that reached **no one** — say so, do not assume.
125
+ Acknowledge a mention once you have acted on it, or it comes back in every session.
126
+
127
+ ## `clearly.md` — what is settled
128
+
129
+ Not a document like the others. It holds what this place IS (the work, the constraints, the
130
+ vocabulary, the decisions that stopped moving) and **how this person wants to be worked with** —
131
+ length, tone, format, and what irritates them, which is said once in passing and worth more than
132
+ anything they praise.
133
+
134
+ Fold a conclusion into it once it has stopped moving. Reach for `edit`, never `replace`.
135
+
136
+ ## Report what MOVED
137
+
138
+ *"You added a paragraph about the 12-hour window to Pricing and removed the line about three
139
+ credits"* is a report. *"I've updated the note"* is not — it costs a turn and says nothing they
140
+ could not have assumed.
141
+
142
+ ## What not to do
143
+
144
+ - **Do not tidy unasked.** A document that looks redundant or superseded is still theirs. If two
145
+ say the same thing, say so and let them choose.
146
+ - **`delete` archives, it does not erase** — the document moves to `~/.archive/`, stays readable
147
+ and comes back. Report it as **archived**; "deleted" makes someone believe something is gone
148
+ that is not.
149
+ - **Do not invent a media line.** `![caption](url)` renders a photo, audio or video by file
150
+ extension. You cannot upload, so never write one pointing at a URL you guessed — it renders
151
+ broken, which reads as their photo being lost. Move existing lines; never fabricate one.
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: clearly-init
3
+ description: >-
4
+ Connect a Clearly workspace to Claude Code or Codex and prove both the MCP and
5
+ CLI paths work. Prefer browser OAuth for remote HTTP clients; when an agent-bound
6
+ bearer credential is required, mint and install it with the active `beehaven`
7
+ identity. Use FIRST whenever a Clearly tool is refused for auth, returns nothing
8
+ on a workspace that should have content, or the user has never connected.
9
+ Triggers: "connect Clearly", "set up Clearly", "sign in to Clearly",
10
+ "authenticate", "install the MCP", "add the MCP token", "it says unauthorized",
11
+ "the tools are not showing up", "/mcp shows nothing", "which workspace am I in".
12
+ ---
13
+
14
+ > **Tool names below are written UNPREFIXED** (`clearly_read`). Your runtime may
15
+ > expose them with a server prefix — e.g.
16
+ > `mcp__plugin_clearly_clearly-staging__clearly_read`. **Match by suffix, not by
17
+ > exact name**: a skill written against the bare name resolves to nothing otherwise, and
18
+ > the failure looks like "the tool doesn't exist" rather than "the name is decorated".
19
+ >
20
+ > **If no such tool is callable at all**, the Clearly MCP server isn't authorised in this
21
+ > session — note that these skills still LIST when it isn't, so you find out by firing a
22
+ > dead call. Authorise it (`/mcp`, or `claude mcp`), or if you have a shell, use the
23
+ > `beehaven` CLI and its own `clearly-canvas` skill instead.
24
+
25
+ # `/clearly:init` — connect a Clearly workspace to Claude Code or Codex
26
+
27
+ Run this once per machine. The plugin's `.mcp.json` points at
28
+ `https://relay.clearly.sh/mcp`; complete authentication and verify the connection before doing
29
+ workspace work.
30
+
31
+ ## Steps
32
+
33
+ ### 1. Authenticate (OAuth — browser sign-in)
34
+
35
+ The plugin already wires the MCP server. Prefer browser OAuth:
36
+
37
+ - **In Claude Code (recommended)**: run `/mcp`, select `clearly`, and choose **Authenticate**. A browser window opens for sign-in; approve and it closes itself. No token.
38
+ - **From the shell**: `claude mcp login clearly` — same browser sign-in flow.
39
+ - **In Codex**: `codex mcp login clearly`. If the server was added outside the plugin first, run
40
+ `codex mcp add clearly --url https://relay.clearly.sh/mcp`, then log in.
41
+
42
+ The sign-in grants scoped access (`rpc:read` for search and read; add `rpc:write` to let the
43
+ agent write documents and create boards or tickets). Sign out or revoke with the client's MCP
44
+ logout command, or from Settings → **Developers** in the app.
45
+
46
+ ### Agent-bound token path (CLI and automation)
47
+
48
+ Use this when the client cannot complete OAuth or when the credential must act as a specific agent
49
+ identity. Sign in and select the identity first:
50
+
51
+ ```bash
52
+ beehaven agent login <agent-name>
53
+ beehaven connect home
54
+ beehaven agent whoami
55
+ beehaven mcp install --client codex
56
+ ```
57
+
58
+ The installer writes through the client's own config command and never prints the token. Restart
59
+ the client afterward. A static token normally carries `rpc:read` and `rpc:write`; `agent:ask` is
60
+ OAuth-only and `rpc:admin` is reserved for configured platform admins. Never paste a bearer token
61
+ into a document, ticket, canvas, log, or chat response.
62
+
63
+ > **Working in more than one workspace?** A client stores one credential per server entry, so two
64
+ > workspaces behind a single entry collide. Use `https://relay.clearly.sh/mcp/w/<workspaceId>` as a
65
+ > second entry — see [the MCP docs](https://clearly.sh/docs/mcp).
66
+
67
+ ### 2. Verify
68
+
69
+ Run `/mcp` in Claude Code, or `codex mcp list` in Codex. You should see `clearly` authenticated
70
+ with **7 tools**:
71
+
72
+ **Seven MCP tools, split by what they DO rather than by what they act on.** The four CRUD tools
73
+ cover every artifact type — document, canvas, sheet, deck, board, ticket, project — so the tool fixes
74
+ whether it reads or writes and `type` says what it touches.
75
+
76
+ - **`clearly_catalog`** — optional, lazy machine-readable schemas. Use `{ type, operation }` before a type-specific write/edit; omit both for the compact capability overview.
77
+ - **`clearly_read` / `clearly_create` / `clearly_edit` / `clearly_replace` / `clearly_delete`** — open one, make a new one, change part of one, overwrite one's whole body, archive one. Each takes a uniform same-operation `batch`.
78
+ - **`clearly_comment`** — comment on any item (with @mentions), or send and reply to messages.
79
+ - **`clearly_grep` / `clearly_glob`** — find by content, find by name. Both span notes and code.
80
+
81
+ Addressed by `target`: a path (`~/Q1/Plan.md`), a key (`CLR-42`) or an id. Folders are projects,
82
+ documents are `.md`, canvases are `.scene.json` — the extension names the type, and you pass `type`
83
+ when it does not.
84
+
85
+ **Everything else is the `beehaven` CLI**, by action name — the canvas, the Company Brain, skills,
86
+ scheduling, stickers, billing. `beehaven actions` lists them; `beehaven call <action> '<json>'` runs
87
+ one:
88
+
89
+ ```bash
90
+ beehaven call context-search '{"query":"per-seat billing","scope":"org"}'
91
+ beehaven call canvas-act '{"compositionId":"c_…","batch":[…]}'
92
+ ```
93
+
94
+ ⚠ **That split is deliberate, not a gap.** A single tool that runs whatever action the caller names
95
+ spans safe and unsafe operations at once — a connector-directory rejection criterion, and one the
96
+ criteria explicitly refuse to let a description paper over. So the generic door is the shell, where
97
+ Claude Code already lives and where every call is attributed to your agent.
98
+
99
+ > ⚠ **Everything requires authentication.** There is no anonymous surface: as of 2026-09-11 both
100
+ > `tools/list` and `tools/call` resolve a credential first, and every credential is bound to an
101
+ > agent. A 401 carries an OAuth challenge, so a capable client can recover from it by itself —
102
+ > but a caller with no token sees no catalog, by design.
103
+
104
+ ## Install the CLI — the other half of the surface
105
+
106
+ The seven MCP tools cover reading, writing and searching artifacts. **Everything else — the canvas,
107
+ the Company Brain, skills, stickers — is reached with `beehaven`**, and the skills in this plugin
108
+ assume it is on PATH. Claude Code has a shell, so this is one command:
109
+
110
+ ```bash
111
+ curl -fsSL https://clearly.sh/install.sh | sh
112
+ beehaven login # once, opens a browser
113
+ beehaven status # daemon + relay, both should read UP
114
+ ```
115
+
116
+ ⚠ **`beehaven: command not found` is the failure to expect**, and it does not look like a missing
117
+ install — it looks like the skill being wrong. If a `beehaven call …` line fails that way, install
118
+ it rather than reaching for an MCP tool that does not exist.
119
+
120
+ ⚠ **Do not restart the user's daemon to fix something.** It is theirs; `beehaven status` tells you
121
+ what is wrong and the message says which command fixes it.
122
+
123
+ Then load the usage skill: it teaches the Company Brain + self-prompting loops — see `clearly-workflows`.
124
+
125
+ A quick functional test:
126
+ > "Map my Clearly workspace's brain using the clearly MCP."
127
+
128
+ If Claude calls `clearly_catalog` and gets back the artifact types with their capabilities, the connection is live. It is a connection check and schema lookup, not a required preflight for ordinary reads/deletes.
129
+
130
+ ## Troubleshooting
131
+
132
+ - **`Missing scope "rpc:write"`** — the sign-in didn't grant write access. Run `claude mcp logout clearly`, then re-authenticate (`/mcp` → **Authenticate**) and approve the write scope.
133
+ - **`Not authenticated` / calls rejected** — the session expired or was revoked. Re-authenticate with `claude mcp login clearly` (or `/mcp` → **Authenticate**).
134
+ - **`clearly` shows in `/mcp` but as unauthenticated** — the endpoint is preconfigured by the plugin; you still need to complete the browser sign-in. Select `clearly` → **Authenticate**.
135
+ - **`Network error reaching ...`** — relay.clearly.sh might be down (rare); check https://clearly.sh status page.
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: clearly-workflows
3
+ description: >-
4
+ Use the Clearly workspace as a COMPANY BRAIN — search the org's accumulated
5
+ context (decisions, facts, prompts, docs), write conclusions back so the next
6
+ agent inherits them instead of re-deriving them, and inherit the team's
7
+ skills. Use when a question is about what this company already decided or
8
+ knows, and after you work something out that the next person would otherwise
9
+ rediscover. Triggers: "what did we decide", "why did we do it this way", "has
10
+ anyone looked at this", "remember this", "write that down", "what do we know
11
+ about X", "company context", "our decisions", "prior art", "institutional
12
+ knowledge", "so nobody has to work this out again".
13
+ ---
14
+
15
+ > **Tool names below are written UNPREFIXED** (`clearly_read`). Your runtime may
16
+ > expose them with a server prefix — e.g.
17
+ > `mcp__plugin_clearly_clearly-staging__clearly_read`. **Match by suffix, not by
18
+ > exact name**: a skill written against the bare name resolves to nothing otherwise, and
19
+ > the failure looks like "the tool doesn't exist" rather than "the name is decorated".
20
+ >
21
+ > **If no such tool is callable at all**, the Clearly MCP server isn't authorised in this
22
+ > session — note that these skills still LIST when it isn't, so you find out by firing a
23
+ > dead call. Authorise it (`/mcp`, or `claude mcp`), or if you have a shell, use the
24
+ > `beehaven` CLI and its own `clearly-canvas` skill instead.
25
+
26
+ # Working a Clearly workspace (Company Brain)
27
+
28
+ Clearly is a **company brain over MCP**: every prompt + agent response is auto-cataloged into a searchable, ranked archive, with documents, decisions, and durable facts alongside. Treat it the way you'd treat a repo — read before you write, and write so the next agent inherits your work.
29
+
30
+ If the `clearly` server isn't connected yet, run the `clearly-init` skill first.
31
+
32
+ ## 1. Orient before you act
33
+
34
+ Run `beehaven call context-map` once at the start. It returns the brain's size (prompts / responses / facts), the recurring topics, and the org/teams it can federate across. Use it to decide whether the answer already exists.
35
+
36
+ ## 2. Retrieve context (don't re-derive it)
37
+
38
+ `beehaven call context-search '{ "query": …, "scope"?, "kinds"?, "limit"? }'` is ONE ranked search across prompts, agent responses, documents/PRDs, and facts — use it instead of guessing or asking the user what they already decided.
39
+
40
+ - `scope: "workspace"` (default) searches this workspace.
41
+ - `scope: "org"` **federates across every workspace in the org**, merges + re-ranks by relevance, and excludes private artifacts + personal facts. Use it for "what has anyone on the team decided/built about X".
42
+ - `kinds`: narrow to `["document"]`, `["prompt"]`, etc.
43
+
44
+ Each hit carries `kind`, `ref` (the id to fetch in full), `title`, `snippet`, `score`, and `meta` (intent, tags, `sourceWorkspace`).
45
+
46
+ **Always search before writing** — it prevents duplicate PRDs/decisions and surfaces prior context you'd otherwise miss.
47
+
48
+ ## 3. Write context BACK so it compounds
49
+
50
+ When you produce something durable — a PRD, a decision, a spec, a research summary — file it with `context-write`:
51
+
52
+ ```jsonc
53
+ beehaven call context-write '{
54
+ "title": "Per-seat billing decision",
55
+ "body": "We charge per seat; usage is per-seat with shared top-up overflow. Rationale: …",
56
+ "kind": "decision", // doc | note | decision | prd
57
+ "tags": ["billing","pricing"],
58
+ "fact": "Billing is per-seat with shared top-up", // optional one-line takeaway
59
+ "private": false // true → workspace-local, never federated across the org
60
+ }'
61
+ ```
62
+
63
+ It creates the document AND files a searchable catalog entry, so the very next `context-search` finds it. Mark anything sensitive `private: true`.
64
+
65
+ ⚠ **Why these are CLI calls and not MCP tools.** The MCP surface is seven tools split by what they do to an artifact (`clearly_catalog · _read · _write · _edit · _delete · _grep · _glob`); the Company Brain's verbs are workspace ACTIONS, reached by name through the shell. Once a document exists, `clearly_grep` and `clearly_read` open it like any other.
66
+
67
+ ## 4. Inherit the team's skills
68
+
69
+ The workspace has its own skills (procedures the team grew). Before improvising a multi-step task, check whether one exists:
70
+
71
+ - `beehaven call skill-list '{"includeWorkspace":true}'` → the skill cards (name + when-to-use).
72
+ - `beehaven call skill-get '{"id":"…"}'` → the full step-by-step instructions for the one whose trigger matches.
73
+
74
+ Follow the loaded procedure rather than inventing your own — that's how you work *the way this workspace works*.
75
+
76
+ ## Anything else
77
+
78
+ `beehaven actions` lists every workspace action with its description; `beehaven call <action> '<json>'` runs one. Reach for these when the seven MCP tools don't cover what you need — which is most things beyond reading, writing and searching artifacts.
79
+
80
+ ## Rules of thumb
81
+
82
+ - **Read → act → write back.** Search first; file durable outcomes so they persist.
83
+ - **`scope:"org"`** for cross-team questions; **`private:true`** for sensitive writes.
84
+
85
+ <!-- A "Schedule your future self" section documenting `clearly_schedule_wake` was removed on
86
+ 2026-08-02: the tool is in ZERO entries of mcp-server.ts's tool list and `schedule-wake` is
87
+ not a registered RPC, so every example here failed. Restore only when it is in tools/list. -->
88
+
89
+ - **Check `beehaven call skill-list`** before hand-rolling a known procedure.
package/skills/index.json CHANGED
@@ -83,5 +83,40 @@
83
83
  "SKILL.md"
84
84
  ],
85
85
  "sha": "b3f47988cb9235b9"
86
+ },
87
+ {
88
+ "name": "visualize",
89
+ "files": [
90
+ "SKILL.md"
91
+ ],
92
+ "sha": "ce9410c1ba7b3457"
93
+ },
94
+ {
95
+ "name": "sticker-pack",
96
+ "files": [
97
+ "SKILL.md"
98
+ ],
99
+ "sha": "f029503ebae59c64"
100
+ },
101
+ {
102
+ "name": "clearly-docs",
103
+ "files": [
104
+ "SKILL.md"
105
+ ],
106
+ "sha": "388be4de86f59eb4"
107
+ },
108
+ {
109
+ "name": "clearly-init",
110
+ "files": [
111
+ "SKILL.md"
112
+ ],
113
+ "sha": "d4dfdd76c7c0a108"
114
+ },
115
+ {
116
+ "name": "clearly-workflows",
117
+ "files": [
118
+ "SKILL.md"
119
+ ],
120
+ "sha": "f26dbbc90929ebe7"
86
121
  }
87
122
  ]
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: sticker-pack
3
+ description: Turn an idea into a printable, die-cut STICKER SHEET on a Clearly canvas — Clearly's own sticker-vending-machine pipeline surfaced as an agent skill. Generates a cohesive SET of transparent stickers, lays them out as a sheet (frame + gridded stickers + a vector kiss-cut cutline layer), and hands back a print-ready bundle. Load this when the user says "make me stickers / a sticker pack / a sticker sheet about X", "turn this into stickers", "design stickers for my brand / startup / cat", or "I want die-cut stickers". The fun, inherently-shareable corner of the canvas skill set.
4
+ ---
5
+
6
+ > **⚠ CANVAS AND STICKER WORK GO THROUGH THE CLI, NOT AN MCP TOOL.** Anything written
7
+ > `beehaven call <action>` below is a literal command. The MCP surface is seven tools split
8
+ > by operation; the ~1,000 workspace actions are reached by name through the shell.
9
+
10
+ # Sticker pack — idea → printable die-cut sheet
11
+
12
+ Stickers are the most organically viral thing Clearly makes: a generated die-cut pack someone actually prints and slaps on a laptop *is* the share. This skill drives Clearly's real sticker-vending pipeline — the same one behind the studio — straight onto a canvas.
13
+
14
+ Install the `beehaven` CLI, sign in as an agent, and connect the intended workspace first
15
+ (`clearly-init` handles setup). Then read `clearly-canvas` for the primitives and the
16
+ perceive→act→revise loop. This skill adds the **generation** step on top of it.
17
+
18
+ ## The one core tool (and a heads-up)
19
+
20
+ The sticker pipeline runs through **`sticker-sheet-gen`**, a real workspace action. It is NOT (yet) a dedicated MCP tool, so you call it through the generic dispatcher:
21
+
22
+ ```jsonc
23
+ beehaven call sticker-sheet-gen '{ … }'
24
+ ```
25
+
26
+ > ⚠ **This is a CLI action, not an MCP tool, and that is settled rather than pending.**
27
+ > `clearly_sticker_generate` **does not exist** and is not planned — an earlier version of this
28
+ > note called it a missing piece. The MCP surface is seven tools split by operation, and a
29
+ > per-feature tool for every workspace action is the 139-tool surface that was withdrawn. The
30
+ > action is production code — the same pipeline the paid studio uses — and the CLI reaches it with
31
+ > full attribution.
32
+
33
+ **It is slow (30–90s), async, and costs the user a sheet credit.** Pass a `compositionId` and the pipeline does the canvas work for you: when the art lands, it places a **transparent parent frame ("Sticker sheet")**, every cut-out sticker nested as its own `image` node (drag one out!), and the **die-cut contour as a nested vector `svg` layer ("Cutline (kiss-cut)")**. The cutline is true vector — exported as the spot cut layer, never rasterized.
34
+
35
+ The call awaits the pipeline (racing a 90s timeout) so you usually get the **full result back** — `{ ok, sheetId, stickerCount, sheetUrl, placed }`. If it ran long you get `{ sessionId, timedOut: true }`; then perceive until the frame appears (see step 2).
36
+
37
+ ## 1. Brief — turn the idea into a cohesive set
38
+
39
+ Don't just forward the user's sentence. Shape it into a **set with a through-line**: one subject family, one style, one palette, consistent line weight. Then list the concepts back before spending the credit.
40
+
41
+ > *"Stickers for your plant shop — I'm thinking a cohesive 9-sticker set, flat-color cottagecore, sage + terracotta palette, chunky outlines: 1) monstera leaf, 2) watering can, 3) 'plant mom' badge, 4) terracotta pot, 5) snail, 6) sun, 7) 'grow' lettering, 8) seed packet, 9) trowel. Generate this sheet?"*
42
+
43
+ A sticker sheet is ONE generation, not N — the model draws the whole cohesive grid. So the brief = the `prompt` (subject + vibe + palette), plus `style` and `stickerCount`.
44
+
45
+ ## 2. Generate — one call, drop on the canvas
46
+
47
+ Make a canvas first (or reuse one), then fire the gen with `compositionId` so it lands:
48
+
49
+ ```jsonc
50
+ beehaven call composition-create '{ "title": "Plant shop stickers" }'
51
+ // → { "id": "c_…" }
52
+
53
+ beehaven call sticker-sheet-gen '{
54
+ "compositionId": "c_…",
55
+ "prompt": "cottagecore plant-shop stickers: monstera leaf, watering can, terracotta pot, snail, sun, seed packet — flat color, chunky outlines, sage green + terracotta palette",
56
+ "style": "cottagecore",
57
+ "stickerCount": 9,
58
+ "bleedStyle": "halo",
59
+ "aspectRatio": "2:3"
60
+ }'
61
+ // → { ok:true, sheetId:"sht_…", stickerCount:9, sheetUrl:"https://…", placed:{ stickers:9, cutline:true } }
62
+ ```
63
+
64
+ Input shape (all optional except `prompt`):
65
+
66
+ | Field | Meaning |
67
+ |---|---|
68
+ | `prompt`* | what the stickers depict — subject, vibe, palette |
69
+ | `compositionId` | **pass it** — the canvas to drop the sheet onto (placeholder + frame + cutline) |
70
+ | `style` | preset: `kawaii`, `planner`, `cottagecore`, `doodle`, … (omit → inferred) |
71
+ | `stickerCount` | stickers on the sheet (e.g. 9, 16, 30). Default 30 |
72
+ | `bleedStyle` | `halo` (default — white sticker outline) or `full-bleed` |
73
+ | `aspectRatio` | sheet ratio: `1:1` `2:3` `3:2` `3:4` `4:3` `9:16` `16:9` |
74
+ | `quality` | `standard` (default, 4K) or `premium` |
75
+ | `seed` | reproducibility |
76
+
77
+ **Handle the async/loading case.** If the response is `{ sessionId, timedOut: true }` (no `placed`), the gen is still running — the ⏳ placeholder is already on the canvas. Poll until the real frame lands:
78
+
79
+ ```jsonc
80
+ canvas-perceive { "compositionId": "c_…", "format": "text" }
81
+ // look in `contents` for a frame named "Sticker sheet" with image children
82
+ // + an svg layer "Cutline (kiss-cut)". When it's there, the sheet is done.
83
+ ```
84
+
85
+ On a hard failure the response carries an `error` (`no_exports_remaining` → user needs to buy a sheet; `rate_limit_exceeded` → back off `retryAfter`s) and the credit is auto-refunded — relay that plainly, don't retry blindly.
86
+
87
+ ## 3. The sheet is already laid out — make it presentable
88
+
89
+ The pipeline placed the frame, the cut-out stickers (gridded, native pixel size), and the cutline layer in ONE write. Your job is polish, via `canvas-act` (see `clearly-canvas`):
90
+
91
+ - **Perceive first**, read the `backgroundColor`, then drop a `text` title over the frame: "Plant shop · 9 stickers · kiss-cut".
92
+ - A tiny caption noting the **"Cutline (kiss-cut)"** layer is the spot cut path (vector) — toggle it to preview the die-cut.
93
+ - If the grid is cramped, `canvas.update-nodes` to nudge the frame into the user's `gaze`.
94
+
95
+ Don't rebuild the grid by hand — the pipeline already nested everything correctly. Only add labels + framing context.
96
+
97
+ ## 4. Export — the printable bundle
98
+
99
+ When they're happy, pull the print kit:
100
+
101
+ ```jsonc
102
+ beehaven call sticker-sheet-export '{ "sheetId": "sht_…" }'
103
+ // → { zipUrl, pdfUrl, sheetUrl, stickerUrls:[…], stickerCount, hasCutFile:true }
104
+ ```
105
+
106
+ - `zipUrl` — the FULL kit: print-ready **PDF** + **cut-file SVG** (the spot cut layer) + every individual sticker PNG + the composited sheet.
107
+ - `pdfUrl` — drop straight into a Cricut / Roland / Stickermule flow.
108
+ - Omit `sheetId` to export their most recent sheet.
109
+
110
+ Hand the user the `pdfUrl` / `zipUrl` and tell them the SVG cut file is what a plotter reads for the die-cut contour.
111
+
112
+ ## 5. Keep the human in the loop
113
+
114
+ The whole point of the canvas is steering by gesture. After the sheet lands: *"It's on your canvas — tell me 'more like #2', 'swap to a blue palette', or 'add a cat'."* Then re-run `sticker-sheet-gen` with the revised `prompt`/`style`/`seed` (reuse `seed` to keep a look, change it to re-roll), and re-perceive. Iterate until they love it, *then* export.
115
+
116
+ ## The viral bit
117
+
118
+ A real die-cut pack — transparent stickers, a visible kiss-cut contour, a print-ready PDF the user actually sends to a sticker printer — is the single most shareable output Clearly has. People post the laptop, not the prompt. So make every sheet land clean: cohesive set, honest cutline, one screenshot that reads as "I generated these and printed them." The sticker IS the share.
119
+
120
+ ## Single-sticker fallback
121
+
122
+ For ONE vector sticker (not a sheet), draw it natively with `canvas-act` like a designer: shapes and `vector.create` pen paths in a frame, named and grouped, with the white border as its own closed path behind the art. Use `studio-image-generate` for a painterly raster sticker, and `format:'svg'` only when a generated SVG file is asked for. If `sticker-sheet-gen` is ever unavailable, the manual sheet = `studio-image-generate` per concept (transparent), then `canvas-act` → `frame.create` + one `canvas.create-node {type:'image'}` per sticker on a grid + a closed `vector.create` cutline outline. But that's the cold path — the pipeline above is the real, supported one.
123
+
124
+ ## Rules of thumb
125
+
126
+ - **One gen = one whole sheet.** Don't loop the call per sticker; the model draws the cohesive grid in a single pass.
127
+ - **Always pass `compositionId`** — that's what lands the placeholder, frame, stickers, and cutline. No `compositionId` = no canvas output.
128
+ - **It's slow + costs a credit.** Confirm the brief BEFORE generating; relay `no_exports_remaining` / `rate_limit_exceeded` plainly (credit auto-refunds on failure).
129
+ - **`timedOut:true` ≠ failed** — the gen is still running; `canvas-perceive` until the "Sticker sheet" frame appears.
130
+ - **The cutline is vector** ("Cutline (kiss-cut)" svg layer) — it's the spot cut path; the export SVG/PDF is what a plotter cuts.
131
+ - **Iterate by re-genning**, reuse `seed` to hold a look, then **export last**.
132
+ - **Reach `sticker-sheet-gen` / `sticker-sheet-export` with `beehaven call`** — they are workspace actions, not MCP tools, and will not become tools.
@@ -0,0 +1,188 @@
1
+ ---
2
+ name: visualize
3
+ description: Turn any concept, answer, or described system into a clean DIAGRAM on a Clearly spatial canvas instead of a wall of text. Load this when the user asks to "diagram this", "draw it", "sketch it", "map it out", "visualize this",
4
+ "explain visually", "show me how X works", "flowchart this", "make a diagram" — AND proactively whenever YOUR OWN answer would be clearer as a picture than a paragraph (a flow with branches, who-calls-whom, a schema, an architecture, options on axes). Picks the right diagram type for the content and builds it with the canvas primitives.
5
+ ---
6
+
7
+ > **⚠ CANVAS WORK GOES THROUGH THE CLI, NOT AN MCP TOOL.** Anything written
8
+ > `canvas-act { … }` / `canvas-perceive { … }` below is an ACTION, run as
9
+ > `beehaven call canvas-act '<the JSON>'`. The payloads are exactly as shown. The MCP
10
+ > surface is seven tools split by operation, and a single tool that dispatches 230
11
+ > caller-named canvas actions spans reads, writes and deletes at once — which is why the
12
+ > generic door is the shell instead. `clearly_read { target, type: "composition" }` still
13
+ > reads a canvas as an artifact.
14
+
15
+ # Visualize — draw it, don't write it
16
+
17
+ Some answers are paragraphs. Many are **pictures pretending to be paragraphs**: a flow with branches, a request hopping between services, a schema, a tradeoff matrix. The moment relationships matter, prose makes the reader rebuild the shape in their head. A diagram hands them the shape — spatial layout *is* the explanation, it's glanceable, and it's screenshot-shareable in a way a wall of text never is.
18
+
19
+ So: when you catch yourself about to write three paragraphs explaining how something connects, **draw it on the canvas instead** (and you can still say the one-line version in chat).
20
+
21
+ Prereq: the `beehaven` CLI is installed, signed in as an agent, and connected to the intended
22
+ workspace (run `clearly-init` if not). **Read `clearly-canvas` for the tool contract** —
23
+ `canvas-perceive` / `canvas-act` / `canvas-catalog`, `frame.create`, `canvas.create-node`
24
+ (text/rect/ellipse/svg/line), first-class `arrow.create`, and nesting via `parentId` with
25
+ frame-relative coordinates. This skill is the *diagram cookbook* on top of those primitives.
26
+
27
+ ## Pick the type (1-line heuristic each)
28
+
29
+ | If the content is… | Draw a… |
30
+ |---|---|
31
+ | an answer you'd write as multiple paragraphs | **Explain-an-answer** (boxes + arrows) |
32
+ | steps, especially with branches | **Flowchart** (boxes + decision diamonds) |
33
+ | who-talks-to-whom over time | **Sequence diagram** (lifelines + message arrows) |
34
+ | services / data stores and their calls | **Architecture** (frames + cylinders + arrows) |
35
+ | things and the relationships between them | **ER / schema** (entity boxes + cardinality lines) |
36
+ | one idea exploding outward into subtopics | **Mind-map** (central node + radial branches) |
37
+ | options scored on a few criteria | **Comparison matrix** (grid of cells) |
38
+
39
+ When unsure, ask: *is the story "then" (flowchart), "who" (sequence), "what's-related" (ER), or "which-is-better" (matrix)?*
40
+
41
+ ## The build loop (every diagram)
42
+
43
+ 1. **Get a canvas + perceive.** `canvas-perceive { compositionId, format:"text" }` → read `backgroundColor` (design WITH it — light ink on dark, dark ink on light) and `gaze.visibleWorldRect` (place work where the user is looking).
44
+ 2. **Batch the skeleton.** One `canvas-act` with a `batch` of frames + boxes on a grid. Capture every returned `{ id }`.
45
+ 3. **Wire the arrows** with `arrow.create {from, to}` — a first-class arrow that BINDS to the boxes (by returned id OR layer name) and FOLLOWS them when they move; `routing:"elbow"` for orthogonal flowcharts, `label` for the edge text (no separate label node needed). See clearly-canvas → Arrows / connectors.
46
+ 4. **Label** edges/branches with small `text` nodes at arrow midpoints.
47
+ 5. **Re-perceive** so the user can ink notes; read them back; revise with `canvas.update-nodes`.
48
+
49
+ Grid discipline matters more than anything: equal gutters (24–40px), aligned edges, generous whitespace. A diagram that breathes reads as designed.
50
+
51
+ ---
52
+
53
+ ## Recipe: Explain-an-answer
54
+
55
+ Your default upgrade from prose. **Layout:** 3–6 labelled boxes left→right (or top→down) in the order you'd say them, one arrow per "and then / because / leads to", a title text on top.
56
+
57
+ ```jsonc
58
+ canvas-act { "compositionId": "c_…", "batch": [
59
+ { "action": "canvas.create-node", "args": { "type":"text", "name":"title", "text":"How a request gets billed", "x":0, "y":-40, "size":24, "fontWeight":700, "fill":"#fff" } },
60
+ { "action": "canvas.create-node", "args": { "type":"rect", "name":"b1", "x":0, "y":0, "w":180, "h":72, "fill":"#1b2230", "stroke":"#3a465c", "radius":10 } },
61
+ { "action": "canvas.create-node", "args": { "type":"text", "name":"b1t", "text":"Member acts", "x":16, "y":26, "size":15, "fontWeight":600, "fill":"#e8edf5" } },
62
+ { "action": "canvas.create-node", "args": { "type":"rect", "name":"b2", "x":260, "y":0, "w":180, "h":72, "fill":"#1b2230", "stroke":"#3a465c", "radius":10 } },
63
+ { "action": "canvas.create-node", "args": { "type":"text", "name":"b2t", "text":"Debit seat budget", "x":276, "y":26, "size":15, "fontWeight":600, "fill":"#e8edf5" } },
64
+ { "action": "arrow.create", "args": { "from":"b1", "to":"b2", "routing":"elbow" } }
65
+ ]}
66
+ ```
67
+
68
+ Chain more boxes at `x += 260` per step. Branch (e.g. "budget exhausted → top-up") with a second arrow going down to an offset box.
69
+
70
+ ## Recipe: Flowchart / process
71
+
72
+ **Layout:** boxes top→down (or left→right), **rounded rect = action**, **diamond = decision**. Draw a diamond natively as a closed `vector.create` rhombus (4 points: top, right, bottom, left); give it two outgoing arrows labelled **Yes / No**.
73
+
74
+ ```jsonc
75
+ canvas-act { "compositionId":"c_…", "batch":[
76
+ { "action":"canvas.create-node", "args":{ "type":"rect", "name":"start", "x":120, "y":0, "w":160, "h":56, "fill":"#173a2a", "stroke":"#2f6b4d", "radius":28 } },
77
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"startt", "text":"Invite sent", "x":156, "y":18, "size":14, "fontWeight":600, "fill":"#d6f5e4" } },
78
+ { "action": "arrow.create", "args": { "from":"start", "to":"decide", "label":"yes", "routing":"elbow" } },
79
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"yesl", "text":"yes", "x":216, "y":74, "size":12, "fontWeight":600, "fill":"#7c8497" } }
80
+ ]}
81
+ ```
82
+
83
+ Rules: one entry, decisions are the only nodes with >1 exit, **every arrow labelled at branches**, keep the happy path straight and push error/No branches to the side.
84
+
85
+ ## Recipe: Sequence diagram
86
+
87
+ **Layout:** each actor is a column — a header box at top + a vertical lifeline (`type:"line"`) running down. Messages are **horizontal arrows** between lifelines; **time flows down**, so each later message sits at a larger `y`.
88
+
89
+ ```jsonc
90
+ canvas-act { "compositionId":"c_…", "batch":[
91
+ { "action":"canvas.create-node", "args":{ "type":"rect", "name":"a1", "x":0, "y":0, "w":120, "h":40, "fill":"#1b2230", "stroke":"#3a465c", "radius":8 } },
92
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"a1t", "text":"Web", "x":48, "y":12, "size":14, "fontWeight":600, "fill":"#e8edf5" } },
93
+ { "action":"canvas.create-node", "args":{ "type":"line", "name":"a1life", "x":60, "y":40, "w":0, "h":300, "stroke":"#3a465c" } },
94
+ { "action":"canvas.create-node", "args":{ "type":"rect", "name":"a2", "x":280, "y":0, "w":120, "h":40, "fill":"#1b2230", "stroke":"#3a465c", "radius":8 } },
95
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"a2t", "text":"Home DO", "x":312, "y":12, "size":14, "fontWeight":600, "fill":"#e8edf5" } },
96
+ { "action":"canvas.create-node", "args":{ "type":"line", "name":"a2life", "x":340, "y":40, "w":0, "h":300, "stroke":"#3a465c" } },
97
+ { "action": "arrow.create", "args": { "from":"a1life", "to":"a2life", "label":"check-quota(userId)" } },
98
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"m1t", "text":"check-quota(userId)", "x":120, "y":52, "size":12, "fontWeight":500, "fill":"#aab3c2" } }
99
+ ]}
100
+ ```
101
+
102
+ Place the message text just above its arrow. A return/response arrow points back left (flip `x1`/`x2`). Keep columns evenly spaced (~280px) so long labels fit.
103
+
104
+ ## Recipe: Architecture / system diagram
105
+
106
+ **Layout:** each service is a `frame` (so it can hold a label + sub-parts); **data stores are cylinders** drawn natively (a `rect` body with an `ellipse` cap on top and one behind at the bottom, grouped with `canvas.group`); arrows are calls. Cluster by tier — a row of frames per tier (client / edge / data), with a faint heading text per row.
107
+
108
+ ```jsonc
109
+ canvas-act { "compositionId":"c_…", "batch":[
110
+ { "action":"frame.create", "args":{ "name":"Worker", "x":0, "y":80, "w":200, "h":110, "fill":"#141a24" } },
111
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"wl", "text":"CF Worker", "x":14, "y":12, "size":14, "fontWeight":700, "fill":"#e8edf5", "parentId":"<WorkerFrameId>" } },
112
+ { "action": "arrow.create", "args": { "from":"Worker", "to":"HomeDO", "routing":"elbow", "label":"rpc" } }
113
+ ]}
114
+ ```
115
+
116
+ Rules: one box per service (not per file), label every arrow with the call/protocol if non-obvious, keep tiers as aligned rows so dataflow reads in one direction.
117
+
118
+ ## Recipe: ER / schema
119
+
120
+ **Layout:** each entity is a `frame` with a bold title row + a `text` node per field (stack fields at `y += 20`). Relationships are lines between frames with a **cardinality label** (`1`, `N`, `1..*`) near each end.
121
+
122
+ ```jsonc
123
+ canvas-act { "compositionId":"c_…", "batch":[
124
+ { "action":"frame.create", "args":{ "name":"User", "x":0, "y":0, "w":180, "h":120, "fill":"#141a24" } },
125
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"u-h", "text":"User", "x":12, "y":10, "size":15, "fontWeight":700, "fill":"#fff", "parentId":"<UserFrameId>" } },
126
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"u-f1", "text":"id PK", "x":12, "y":40, "size":13, "fontWeight":400, "fill":"#aab3c2", "parentId":"<UserFrameId>" } },
127
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"u-f2", "text":"email", "x":12, "y":60, "size":13, "fontWeight":400, "fill":"#aab3c2", "parentId":"<UserFrameId>" } },
128
+ { "action":"frame.create", "args":{ "name":"Seat", "x":300, "y":0, "w":180, "h":120, "fill":"#141a24" } },
129
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"s-h", "text":"Seat", "x":12, "y":10, "size":15, "fontWeight":700, "fill":"#fff", "parentId":"<SeatFrameId>" } },
130
+ { "action":"canvas.create-node", "args":{ "type":"line", "name":"u—s", "x":180, "y":40, "w":120, "h":0, "stroke":"#7c8497" } },
131
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"card1", "text":"1", "x":188, "y":24, "size":12, "fontWeight":700, "fill":"#7c8497" } },
132
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"cardN", "text":"N", "x":284, "y":24, "size":12, "fontWeight":700, "fill":"#7c8497" } }
133
+ ]}
134
+ ```
135
+
136
+ Rules: PK/FK noted in the field text, one relationship line per FK, cardinality at BOTH ends so the reader knows which side is the "many".
137
+
138
+ ## Recipe: Mind-map
139
+
140
+ **Layout:** one central node (an `ellipse` or pill `rect`), child topics radiating out, each connected by a **curved** branch (an open `vector.create` pen path with `hIn`/`hOut` control points, not a straight line). Place children evenly around the center; second-level topics hang off their parent.
141
+
142
+ ```jsonc
143
+ canvas-act { "compositionId":"c_…", "batch":[
144
+ { "action":"canvas.create-node", "args":{ "type":"ellipse", "name":"core", "x":220, "y":160, "w":160, "h":80, "fill":"#2a1f3a", "stroke":"#7b5cc4" } },
145
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"coret", "text":"Company Brain", "x":250, "y":190, "size":15, "fontWeight":700, "fill":"#e9defb" } },
146
+ { "action":"canvas.create-node", "args":{ "type":"rect", "name":"c1", "x":480, "y":60, "w":150, "h":48, "fill":"#1b2230", "stroke":"#3a465c", "radius":24 } },
147
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"c1t", "text":"Org context", "x":500, "y":76, "size":13, "fontWeight":600, "fill":"#e8edf5" } },
148
+ { "action":"vector.create", "args":{ "name":"core→c1", "closed":false, "stroke":"#7b5cc4", "strokeWidth":2,
149
+ "points":[ { "x":382, "y":192, "hOut":{ "x":430, "y":192 } }, { "x":478, "y":92, "hIn":{ "x":430, "y":92 } } ] } }
150
+ ]}
151
+ ```
152
+
153
+ Rules: keep the center visually heaviest, ~4–7 first-level branches (more → it's really a tree, use a frame layout), curve the branches so it reads organic not boxy.
154
+
155
+ ## Recipe: Comparison matrix
156
+
157
+ Perfect for **"X vs Y vs Z"**. **Layout:** a grid of `rect` cells — top row = options, left column = criteria, body cells = the values. Tint the header row/column so the axes pop; fill body cells (green/amber/red) to make the winner obvious at a glance.
158
+
159
+ ```jsonc
160
+ canvas-act { "compositionId":"c_…", "batch":[
161
+ { "action":"canvas.create-node", "args":{ "type":"rect", "name":"h-free", "x":160, "y":0, "w":120, "h":40, "fill":"#222b3a", "stroke":"#3a465c" } },
162
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"h-freet", "text":"Free", "x":196, "y":12, "size":14, "fontWeight":700, "fill":"#fff" } },
163
+ { "action":"canvas.create-node", "args":{ "type":"rect", "name":"h-pro", "x":280, "y":0, "w":120, "h":40, "fill":"#222b3a", "stroke":"#3a465c" } },
164
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"h-prot", "text":"Pro", "x":322, "y":12, "size":14, "fontWeight":700, "fill":"#fff" } },
165
+ { "action":"canvas.create-node", "args":{ "type":"rect", "name":"r-agents", "x":0, "y":40, "w":160, "h":40, "fill":"#222b3a", "stroke":"#3a465c" } },
166
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"r-agentst", "text":"Agents", "x":14, "y":52, "size":13, "fontWeight":600, "fill":"#cfd6e2" } },
167
+ { "action":"canvas.create-node", "args":{ "type":"rect", "name":"cell-free-agents", "x":160, "y":40, "w":120, "h":40, "fill":"#3a2417", "stroke":"#3a465c" } },
168
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"cell-free-agentst", "text":"1", "x":214, "y":52, "size":13, "fontWeight":600, "fill":"#f0c9a8" } },
169
+ { "action":"canvas.create-node", "args":{ "type":"rect", "name":"cell-pro-agents", "x":280, "y":40, "w":120, "h":40, "fill":"#173a2a", "stroke":"#3a465c" } },
170
+ { "action":"canvas.create-node", "args":{ "type":"text", "name":"cell-pro-agentst", "text":"10", "x":332, "y":52, "size":13, "fontWeight":600, "fill":"#a8f0c9" } }
171
+ ]}
172
+ ```
173
+
174
+ Keep cells a uniform `w`/`h` and snap every cell to the grid (column `x`, row `y`); the alignment is what makes it read as a table, not scattered boxes.
175
+
176
+ ## The viral bit
177
+
178
+ A clean diagram is **instantly screenshot-shareable** in a way a paragraph never is. Every text answer you convert into a tidy flowchart or architecture map is a tiny ad for the canvas — someone drops it in Slack and the reply is "wait, the AI just *drew* that?". So make each one frame-worthy: aligned grid, honest arrows, one clear read direction, a title. The diagram sells the next person on the surface.
179
+
180
+ ## Rules of thumb
181
+
182
+ - **Prose smell test:** about to write 3+ paragraphs about how things connect? Draw it instead.
183
+ - **Pick by the question:** "then" → flowchart, "who/when" → sequence, "what's related" → ER, "which is better" → matrix, "one idea outward" → mind-map.
184
+ - **Skeleton in one `batch`**, then arrows, then labels — capture every returned `id`.
185
+ - **Arrows = `arrow.create {from,to}`** — a first-class arrow that BINDS to the boxes and follows them (`routing:"elbow"` for flowcharts, `label` for edge text). Never hand-draw one as `type:"svg"` with a `marker-end`: that is a static path that does not move with its nodes; always pass `compositionId`; `name` on every create.
186
+ - **Grid or it's a dump:** equal gutters, aligned edges, one read direction, generous whitespace.
187
+ - **Perceive first** (design with `backgroundColor`, place in `gaze`); **re-perceive after** to read the human's ink and revise.
188
+ - **Need an action not in clearly-canvas?** `canvas-catalog { surface:"all" }` — don't invent action names.