@clearlysh/beehaven 0.8.16

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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. package/skills/pair-on-canvas/SKILL.md +92 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * THE VERB SURFACE — what each verb is, and why it is allowed to exist.
3
+ *
4
+ * ⚠⚠ THIS FILE EXISTS BECAUSE THE QUESTION "should this all just be `call <action> <json>`?" HAD
5
+ * NO ANSWER, ONLY OPINIONS. The CLI carries 113 case labels / ~98 distinct verbs; `beehaven call`
6
+ * already reaches every one of ~1,000 registered actions; and nothing anywhere recorded which of
7
+ * the two doors people actually use. `agent_activity.source` now records it — but a column that
8
+ * fills up is only half a decision procedure. This is the other half: the RULE, written down
9
+ * before the data arrives, so the answer is not chosen after the fact to suit whatever the numbers
10
+ * happen to say.
11
+ *
12
+ * ⚠ MODELLED ON `EAGER_TOOLS`, DELIBERATELY. That list solved the same problem for MCP — 139 tools
13
+ * cut to 20 — by making every survivor state its reason, and the cut was decided on
14
+ * `mcp_tool_calls` rather than taste. Same shape here.
15
+ *
16
+ * ── THE EARNING TEST ────────────────────────────────────────────────────────────────────────
17
+ *
18
+ * A verb that reaches the workspace survives only if it does at least one of:
19
+ *
20
+ * STATE it carries client-side state `call` structurally cannot — a cwd, argument quoting,
21
+ * a fallback ladder. `beehaven ls` is not sugar: it threads a per-target cwd through
22
+ * `shell-session.ts` and quotes arguments that a remote tokenizer would re-split.
23
+ * TRAFFIC it is in the measured top-20 by real traffic. Concentration is extreme — 20 actions
24
+ * carry 84.7% of 16,161 calls — so this is a short list by construction.
25
+ * BEYOND it expresses something no single action can: a multi-call flow, a local file, an
26
+ * interactive loop.
27
+ *
28
+ * ⚠⚠ "IT READS NICER" IS NOT ON THE LIST, and that is the whole point of writing the test down.
29
+ * Every verb reads nicer than `call`; if that counted, nothing would ever be retired and the
30
+ * surface would only grow — which is how it reached 98.
31
+ *
32
+ * ⚠ `STRUCTURAL` IS NOT AN EARNING CATEGORY — IT IS THE FLOOR. Those verbs never reach the
33
+ * workspace at all (`call` presupposes them: a daemon, an account, an identity, a target, the
34
+ * local disk, a transport, an audio device), so the test does not apply and cannot retire them.
35
+ * `AGENT_LOGIN_EXEMPT` in `cli.ts` was derived independently, for the same reason, and the two
36
+ * lists agreeing is the evidence that the boundary is real rather than taste.
37
+ *
38
+ * ⚠⚠ NOTHING IS RETIRED BY THIS FILE. `PROVISIONAL` verbs are the candidates — every one of them
39
+ * is a remote verb with no STATE/BEYOND claim, whose fate rests on TRAFFIC, and traffic per DOOR
40
+ * has been measurable for exactly as long as `agent_activity.source` has existed. Retiring them
41
+ * now would be doing the thing this file was written to stop: deciding on taste and calling it
42
+ * measurement. Re-run `verbTrafficQuery()` once the column has a few weeks on it.
43
+ */
44
+ /**
45
+ * ⚠ EVERY top-level `case` label in `cli.ts` must appear here — `verb-surface.test.ts` reads the
46
+ * switch and fails on any label this file does not classify. A new verb therefore cannot ship
47
+ * without stating what it is, which is the only mechanism that stops the surface drifting back.
48
+ */
49
+ export const VERB_SURFACE = {
50
+ // ── The floor: `call` cannot run without these ────────────────────────────────────────────
51
+ connect: { cls: 'STRUCTURAL', why: 'sets the target every other call routes to' },
52
+ cd: { cls: 'STRUCTURAL', why: 'alias', aliasOf: 'connect' },
53
+ nav: { cls: 'STRUCTURAL', why: 'alias', aliasOf: 'connect' },
54
+ navigate: { cls: 'STRUCTURAL', why: 'alias', aliasOf: 'connect' },
55
+ disconnect: { cls: 'STRUCTURAL', why: 'clears the target' },
56
+ pwd: { cls: 'STRUCTURAL', why: 'reports local session state' },
57
+ where: { cls: 'STRUCTURAL', why: 'alias', aliasOf: 'pwd' },
58
+ env: { cls: 'STRUCTURAL', why: 'chooses which relay the daemon dials' },
59
+ login: { cls: 'STRUCTURAL', why: 'browser OAuth + keychain' },
60
+ logout: { cls: 'STRUCTURAL', why: 'clears local credentials' },
61
+ signup: { cls: 'STRUCTURAL', why: 'opens a browser' },
62
+ whoami: { cls: 'STRUCTURAL', why: 'reads local credentials' },
63
+ start: { cls: 'STRUCTURAL', why: 'spawns the daemon `call` needs' },
64
+ stop: { cls: 'STRUCTURAL', why: 'daemon lifecycle' },
65
+ restart: { cls: 'STRUCTURAL', why: 'daemon lifecycle' },
66
+ daemon: { cls: 'STRUCTURAL', why: 'daemon lifecycle' },
67
+ status: { cls: 'STRUCTURAL', why: 'probes the socket and the relay — cannot use either' },
68
+ doctor: { cls: 'STRUCTURAL', why: 'diagnoses the transport `call` rides on' },
69
+ update: { cls: 'STRUCTURAL', why: 'replaces this binary' },
70
+ completions: { cls: 'STRUCTURAL', why: 'emits a shell script' },
71
+ help: { cls: 'STRUCTURAL', why: 'local text' },
72
+ agent: { cls: 'STRUCTURAL', why: 'mints and activates the identity every call is attributed to' },
73
+ install: { cls: 'STRUCTURAL', why: 'writes hives to local disk' },
74
+ app: { cls: 'STRUCTURAL', why: 'installs/packs/uninstalls local hives' },
75
+ hive: { cls: 'STRUCTURAL', why: 'alias', aliasOf: 'app' },
76
+ apps: { cls: 'STRUCTURAL', why: 'alias', aliasOf: 'app' },
77
+ skills: { cls: 'STRUCTURAL', why: 'writes SKILL.md into local agent homes' },
78
+ marketplace: { cls: 'STRUCTURAL', why: 'installs to local disk' },
79
+ mcp: { cls: 'STRUCTURAL', why: 'writes client config files and mints tokens' },
80
+ 'mcp-config': { cls: 'STRUCTURAL', why: 'prints a client config block' },
81
+ link: { cls: 'STRUCTURAL', why: 'binds a local folder to a project' },
82
+ unlink: { cls: 'STRUCTURAL', why: 'unbinds a local folder' },
83
+ init: { cls: 'STRUCTURAL', why: 'scaffolds files on disk' },
84
+ dev: { cls: 'STRUCTURAL', why: 'runs a local server' },
85
+ deploy: { cls: 'STRUCTURAL', why: 'shells out to wrangler' },
86
+ rollback: { cls: 'STRUCTURAL', why: 'shells out to wrangler' },
87
+ versions: { cls: 'STRUCTURAL', why: 'shells out to wrangler' },
88
+ codegen: { cls: 'STRUCTURAL', why: 'writes generated types to disk' },
89
+ seed: { cls: 'STRUCTURAL', why: 'local project command' },
90
+ migrate: { cls: 'STRUCTURAL', why: 'local project command' },
91
+ test: { cls: 'STRUCTURAL', why: 'local project command' },
92
+ d1: { cls: 'STRUCTURAL', why: 'shells out to wrangler with the local CF token' },
93
+ secrets: { cls: 'STRUCTURAL', why: 'shells out to wrangler' },
94
+ compose: { cls: 'STRUCTURAL', why: 'reads and pushes local composition repos' },
95
+ sync: { cls: 'STRUCTURAL', why: 'uploads local session files' },
96
+ teleport: { cls: 'STRUCTURAL', why: 'reads a file off the local disk' },
97
+ doc: { cls: 'STRUCTURAL', why: 'reads a local file and indexes it' },
98
+ context: { cls: 'STRUCTURAL', why: 'writes .clearly/CLAUDE.md locally' },
99
+ voice: { cls: 'STRUCTURAL', why: 'local audio devices and on-device TTS' },
100
+ say: { cls: 'STRUCTURAL', why: 'drives the local avatar surface' },
101
+ avatar: { cls: 'STRUCTURAL', why: 'drives the local avatar expression surface' },
102
+ speak: { cls: 'STRUCTURAL', why: 'local audio output' },
103
+ pipe: { cls: 'STRUCTURAL', why: 'IS a transport — JSON-RPC over stdio' },
104
+ devices: { cls: 'STRUCTURAL', why: 'local device identity' },
105
+ swarm: { cls: 'STRUCTURAL', why: 'this node\'s role and identity in the mesh' },
106
+ characters: { cls: 'STRUCTURAL', why: 'Mac-app character shop; `call` cannot serve it' },
107
+ moni: { cls: 'STRUCTURAL', why: 'alias', aliasOf: 'characters' },
108
+ treehouse: { cls: 'STRUCTURAL', why: 'opens a local window' },
109
+ claude: { cls: 'STRUCTURAL', why: 'spawns a local Claude session' },
110
+ omni: { cls: 'STRUCTURAL', why: 'interactive local REPL' },
111
+ shell: { cls: 'STRUCTURAL', why: 'interactive loop' },
112
+ watch: { cls: 'STRUCTURAL', why: 'holds a stream open' },
113
+ buildings: { cls: 'STRUCTURAL', why: 'enumerates reachable targets — needed to pick one' },
114
+ workspaces: { cls: 'STRUCTURAL', why: 'enumerates memberships — needed to pick a target' },
115
+ ws: { cls: 'STRUCTURAL', why: 'alias', aliasOf: 'workspaces' },
116
+ // ── Earn their place: client-side state ───────────────────────────────────────────────────
117
+ ls: { cls: 'STATE', why: 'per-target cwd + arg quoting + notes-shell→VFS fallback' },
118
+ cat: { cls: 'STATE', why: 'cwd + quoting; a multi-word title is one argument' },
119
+ grep: { cls: 'STATE', why: 'cwd + quoting; a pattern with spaces is one argument' },
120
+ tree: { cls: 'STATE', why: 'cwd' },
121
+ find: { cls: 'STATE', why: 'cwd' },
122
+ head: { cls: 'STATE', why: 'cwd' },
123
+ tail: { cls: 'STATE', why: 'cwd' },
124
+ wc: { cls: 'STATE', why: 'cwd' },
125
+ jq: { cls: 'STATE', why: 'cwd' },
126
+ rm: { cls: 'STATE', why: 'cwd' },
127
+ mv: { cls: 'STATE', why: 'cwd' },
128
+ cp: { cls: 'STATE', why: 'cwd' },
129
+ mkdir: { cls: 'STATE', why: 'cwd' },
130
+ write: { cls: 'STATE', why: 'cwd + the notes-shell/VFS routing decision' },
131
+ edit: { cls: 'STATE', why: 'cwd + the notes-shell/VFS routing decision' },
132
+ sql: { cls: 'STATE', why: 'rest-of-line quoting — quoting the statement makes the guard refuse it' },
133
+ // ── Earn their place: beyond one action ───────────────────────────────────────────────────
134
+ call: { cls: 'BEYOND', why: 'IS the escape hatch — the floor of the CLI' },
135
+ batch: { cls: 'BEYOND', why: 'scripts multiple workspace actions in one canonical call' },
136
+ 'code-run': { cls: 'BEYOND', why: 'computes between workspace actions in a sandboxed call' },
137
+ actions: { cls: 'BEYOND', why: 'discovery; without it `call` is unusable' },
138
+ explain: { cls: 'BEYOND', why: 'discovery; the authority on an action\'s arguments' },
139
+ describe: { cls: 'BEYOND', why: 'alias', aliasOf: 'explain' },
140
+ rpc: { cls: 'BEYOND', why: 'the raw gateway envelope, below `call`' },
141
+ ticket: { cls: 'BEYOND', why: 'resolves a board before creating, and refuses a numeric priority the API inverts' },
142
+ comment: { cls: 'BEYOND', why: 'addresses ANY surface by key/title and turns per-surface anchor flags into one call; prints who the @s reached' },
143
+ comments: { cls: 'BEYOND', why: 'served by the comment module', aliasOf: 'comment' },
144
+ mention: { cls: 'BEYOND', why: 'resolves an 8-char id prefix before acking, and reads your mention queue as whichever identity you are' },
145
+ mentions: { cls: 'BEYOND', why: 'served by the mention module', aliasOf: 'mention' },
146
+ board: { cls: 'BEYOND', why: 'served by the ticket module', aliasOf: 'ticket' },
147
+ /**
148
+ * ⚠ `sprint` EARNS ITS PLACE ON RESOLUTION, NOT ON REACH. A sprint is addressed by uuid and
149
+ * nothing ever prints one in prose, so the verb resolves a NAME and refuses an ambiguous one;
150
+ * and `sprint plan` resolves card KEYS to row ids before calling, because `sprint-plan` reports
151
+ * `moved: 1` for a key it never moved (measured 2026-09-06).
152
+ */
153
+ sprint: { cls: 'BEYOND', why: 'served by the ticket module', aliasOf: 'ticket' },
154
+ /**
155
+ * ⚠⚠ `restore` EARNS BEYOND ON A DANGEROUS DEFAULT, NOT ON PARSING. `document-archive` treats an
156
+ * OMITTED `archived` as **true**, so the raw one-liner's default is the exact OPPOSITE of what
157
+ * this verb means: a caller who forgets the field archives the thing they asked to restore, and
158
+ * the output is indistinguishable from success. Supplying `archived: false` where the action's
159
+ * default inverts the intent is the same claim `slides move` makes by taking the required `to`
160
+ * positionally — a shape the raw call gets silently wrong.
161
+ *
162
+ * ⚠ It also reports the handler's `unchanged: true` as a NO-OP rather than as "restored".
163
+ */
164
+ restore: { cls: 'BEYOND', why: 'supplies archived:false — the raw action DEFAULTS to true, inverting the intent' },
165
+ /**
166
+ * ⚠ `trash` IS PROVISIONAL, AND THAT IS THE HONEST CLASS FOR IT. It is a listing over
167
+ * `document-list { archived: true }`; the clamp-announcing and the "that is the whole page" line
168
+ * are good manners, not a shape the raw action refuses. Its fate rests on traffic, like every
169
+ * other verb here with no STATE/BEYOND claim.
170
+ *
171
+ * ⚠⚠ BUT IT IS COUPLED TO `restore` AND MUST NOT BE RETIRED ALONE. Both exist because `rm`
172
+ * archives and CBH2-34's reproduction was `beehaven help | grep -iE 'restore|trash'` returning
173
+ * NOTHING. `restore` needs a key, and `trash` is the only surface that prints one. Retire the
174
+ * pair or neither — retiring `trash` on its own leaves an undo nobody can address.
175
+ */
176
+ trash: { cls: 'PROVISIONAL', why: 'a listing; kept because it is the only surface that prints an archived key for `restore`' },
177
+ /**
178
+ * ⚠ `sheet` AND `slides` PARSE ON THE CALLER'S BEHALF, which is what puts them here rather than
179
+ * in PROVISIONAL: `sheet set` turns TSV into the `rows` array `sheet-set` wants and supplies the
180
+ * `at` anchor; `slides add` splits `--bullets` on `|`; `slides move` takes the REQUIRED `to`
181
+ * positionally so it cannot be omitted. Each of those is a shape the raw action refuses or
182
+ * silently ignores, and none of it is expressible as one `call`.
183
+ */
184
+ sheet: { cls: 'BEYOND', why: 'parses TSV into rows + anchor; `sheet-set` takes neither as text' },
185
+ sheets: { cls: 'BEYOND', why: 'alias', aliasOf: 'sheet' },
186
+ slides: { cls: 'BEYOND', why: 'splits bullets, positions the required `to`, separates show from export' },
187
+ deck: { cls: 'BEYOND', why: 'alias', aliasOf: 'slides' },
188
+ /**
189
+ * ⚠ `brand` EARNS ITS PLACE BY REFUSING THE PALETTE-WIPING SHAPE: `brand set <b> color.accent #…`
190
+ * always sends the role MAP, never the array that replaces every colour; it also resolves a name
191
+ * to an id for history/revert. `component` writes `render -o` files and types `place` numbers.
192
+ */
193
+ brand: { cls: 'BEYOND', why: 'builds role-merge patches (never the palette-replacing array); resolves names for history/revert' },
194
+ brands: { cls: 'BEYOND', why: 'alias', aliasOf: 'brand' },
195
+ component: { cls: 'BEYOND', why: 'writes render -o SVG files, types place coordinates, reads spec/patch JSON from @file' },
196
+ components: { cls: 'BEYOND', why: 'alias', aliasOf: 'component' },
197
+ open: { cls: 'BEYOND', why: 'resolves an id across four kinds, then drives an open tab' },
198
+ ask: { cls: 'BEYOND', why: 'a conversation, not a call' },
199
+ shop: { cls: 'BEYOND', why: 'multi-step protocol (find→search→get→checkout) with a safety rule' },
200
+ admin: { cls: 'BEYOND', why: '24 operator subcommands, several multi-call' },
201
+ broadcast: { cls: 'BEYOND', why: 'fans one message across targets' },
202
+ /**
203
+ * ── ⚠⚠ NOT REMOTE AT ALL — CBH2-81 ────────────────────────────────────────────────────────
204
+ *
205
+ * These three were classified as verbs that reach the workspace. They do not, so their
206
+ * `agent_activity` zero is REAL — and retiring them on that zero would be right by accident and
207
+ * wrong by reasoning, which is exactly what this file exists to prevent. Verified by reading each
208
+ * module: `commands/composition.ts` and `commands/tool.ts` contain **zero** `daemonRpc` /
209
+ * `rpcOnce` / `connectClient` calls between them, and `commands/explore.ts` builds every URL as
210
+ * `http://127.0.0.1:${port}`.
211
+ *
212
+ * ⚠ STRUCTURAL, WHICH IS THE FLOOR AND NOT AN EARNING CLAIM. `call` cannot serve a file on this
213
+ * machine or a dev server on this machine, so the earning test does not apply to them and cannot
214
+ * retire them. Same reasoning the header already gives for `pwd` reporting local session state.
215
+ */
216
+ composition: { cls: 'STRUCTURAL', why: 'reads ~/.clearly/composition-repos.json — a local file; no RPC in the module' },
217
+ comp: { cls: 'STRUCTURAL', why: 'alias', aliasOf: 'composition' },
218
+ tool: { cls: 'STRUCTURAL', why: 'reads ~/.beehaven/tools on local disk; no RPC in the module' },
219
+ explore: { cls: 'STRUCTURAL', why: 'targets a LOCAL dev server (127.0.0.1:8787, `beehaven dev`), never the workspace' },
220
+ /**
221
+ * ── ⚠ A DIFFERENT AUTHENTICATION SURFACE — CBH2-81 ────────────────────────────────────────
222
+ *
223
+ * `webhook` and `user` go through `getAdminClient` (`lib/admin-client.ts`): plain HTTP with a
224
+ * worker secret, carrying NO agent identity. So they never pass through `agent-invoke` and
225
+ * structurally cannot write an `agent_activity` row, however much they are used.
226
+ *
227
+ * ⚠ THEIR ZERO MEASURES THE TRANSPORT, NOT THE DEMAND. Left as PROVISIONAL they sit on a
228
+ * retirement list whose criterion cannot see them — the exact failure `ask` is called out for in
229
+ * CBH2-82, one door over. `ADMIN_HTTP` says so, so the traffic reader skips them by class
230
+ * instead of anyone remembering the exception.
231
+ */
232
+ user: { cls: 'ADMIN_HTTP', why: 'getAdminClient — HTTP + worker secret, no agent identity' },
233
+ webhook: { cls: 'ADMIN_HTTP', why: 'getAdminClient — HTTP + worker secret, no agent identity' },
234
+ webhooks: { cls: 'ADMIN_HTTP', why: 'alias', aliasOf: 'webhook' },
235
+ // ── Candidates: no STATE/BEYOND claim; fate rests on measured traffic per door ─────────────
236
+ canvas: { cls: 'PROVISIONAL', why: 'wraps canvas-act/-perceive; `call` reaches both' },
237
+ conversations: { cls: 'PROVISIONAL', why: 'wraps conversation-list' },
238
+ convo: { cls: 'PROVISIONAL', why: 'alias', aliasOf: 'conversations' },
239
+ conv: { cls: 'PROVISIONAL', why: 'alias', aliasOf: 'conversations' },
240
+ library: { cls: 'PROVISIONAL', why: 'wraps library RPCs' },
241
+ lib: { cls: 'PROVISIONAL', why: 'alias', aliasOf: 'library' },
242
+ remember: { cls: 'PROVISIONAL', why: 'one call to agent-memory-remember' },
243
+ recall: { cls: 'PROVISIONAL', why: 'one call to agent-memory-recall' },
244
+ search: { cls: 'PROVISIONAL', why: 'one call; `bash "grep …"` covers the same job' },
245
+ task: { cls: 'PROVISIONAL', why: 'one call to note-create — and its name collides with `ticket`' },
246
+ workspace: { cls: 'PROVISIONAL', why: 'one overview call' },
247
+ logs: { cls: 'PROVISIONAL', why: 'one call' },
248
+ costs: { cls: 'PROVISIONAL', why: 'one call' },
249
+ compliance: { cls: 'PROVISIONAL', why: 'thin wrapper' },
250
+ publish: { cls: 'PROVISIONAL', why: 'one call' },
251
+ 'store-query': { cls: 'PROVISIONAL', why: 'one call' },
252
+ };
253
+ /**
254
+ * The SQL that decides a PROVISIONAL verb's fate, once `agent_activity.source` has run for long
255
+ * enough to be worth reading.
256
+ *
257
+ * ⚠ IT COUNTS THE DOOR, NOT THE ACTION. `ticket-create` is busy either way; the only question is
258
+ * how much of that arrived as `beehaven ticket create` rather than `beehaven call ticket-create`,
259
+ * and before the `source` column nothing could tell them apart.
260
+ *
261
+ * ⚠ A verb with NO rows is not automatically dead — it may simply postdate the column. Check
262
+ * `MIN(timestamp) WHERE source <> ''` before reading a zero as a verdict.
263
+ *
264
+ * ⚠⚠ AND THREE CLASSES OF VERB ARE UNMEASURABLE BY THIS QUERY, SO THEY ARE EXCLUDED FROM IT RATHER
265
+ * THAN LEFT TO BE MISREAD AS DISUSE (CBH2-81, CBH2-82). The exclusion is DERIVED from
266
+ * `VERB_SURFACE` below, not typed here, so a future verb of the same kind is covered on the day it
267
+ * is classified — a hand-kept exception list is how this file's own advice goes stale.
268
+ *
269
+ * · `STRUCTURAL` — never reaches the workspace at all (local disk, a local dev server, the
270
+ * daemon). There is nothing to count and the earning test does not apply.
271
+ * · `ADMIN_HTTP` — `getAdminClient`, plain HTTP with an operator secret and NO agent identity,
272
+ * so it cannot pass through `agent-invoke` and cannot write a row however heavily it is used.
273
+ *
274
+ * ⚠⚠ THE THIRD IS `ask`, AND IT IS THE DANGEROUS ONE, BECAUSE ITS COUNT IS NOT ZERO. `cmdAsk`
275
+ * fires `agent:message` — an IPC `send` frame, not an `rpc` — which the DO records in
276
+ * `conversation_messages` and never in `agent_activity`. **But since 2026-09-04 it falls back to
277
+ * `rpcOnce('agent-prompt', { wait: true })` when the broadcast produces nothing**, and THAT path
278
+ * attributes normally. Measured 2026-09-06: two `ask` invocations took `cli:ask` from 1 to 2.
279
+ *
280
+ * So `cli:ask` counts **how often the stream path failed**, not how often anyone asked. With the
281
+ * Mac app driving a live socket the turn succeeds on the broadcast and writes nothing; headless it
282
+ * falls back and writes a row. ⚠ A PARTIAL, ENVIRONMENT-DEPENDENT COUNT IS WORSE THAN A ZERO — a
283
+ * zero is obviously unusable, whereas `2` looks like a measurement and will be read as one. `ask`
284
+ * is `BEYOND`, so no retirement rests on it either way; it is excluded so nobody derives a
285
+ * usage claim from a number that means something else.
286
+ */
287
+ export function verbTrafficQuery() {
288
+ /**
289
+ * ⚠⚠ ONE DERIVATION, NOT TWO (CBH2-100). This filter used to be computed here AND, separately,
290
+ * inside `unmeasurableVerbs()` — the same predicate written twice, with nothing asserting the
291
+ * two agreed. Proved by narrowing `unmeasurableVerbs` to STRUCTURAL alone and leaving this
292
+ * untouched: `verb-surface-measurability` stayed at 30/30 while the query went on excluding
293
+ * `user` / `webhook` / `webhooks` from the door table and the report no longer explained why
294
+ * they were absent — i.e. exactly the silent drop that function's own docstring exists to
295
+ * prevent, arriving through the function meant to prevent it.
296
+ *
297
+ * The REPORTED reason and the APPLIED exclusion are now the same list by construction.
298
+ */
299
+ const unmeasurable = unmeasurableVerbs().map((e) => e.verb).sort();
300
+ const notIn = unmeasurable.map((v) => `'cli:${v}'`).join(', ');
301
+ return [
302
+ 'SELECT source, COUNT(*) n, COUNT(DISTINCT action) actions',
303
+ 'FROM agent_activity',
304
+ "WHERE source LIKE 'cli:%'",
305
+ `AND source NOT IN (${notIn})`,
306
+ 'GROUP BY source ORDER BY n DESC',
307
+ ].join(' ');
308
+ }
309
+ /**
310
+ * The verbs `verbTrafficQuery()` deliberately cannot see, with the reason each one is invisible.
311
+ *
312
+ * ⚠ EXPORTED SO THE EXCLUSION IS REPORTABLE, NOT JUST APPLIED. A query that silently drops rows is
313
+ * the same shape as the defects this whole cluster is about; anyone reading the door table needs
314
+ * to be able to say WHY a verb is absent from it.
315
+ */
316
+ export function unmeasurableVerbs() {
317
+ const out = Object.entries(VERB_SURFACE)
318
+ .filter(([, f]) => f.cls === 'STRUCTURAL' || f.cls === 'ADMIN_HTTP')
319
+ .map(([verb, f]) => ({
320
+ verb,
321
+ why: f.cls === 'ADMIN_HTTP'
322
+ ? 'getAdminClient — no agent identity, so no agent_activity row is ever written'
323
+ : 'never reaches the workspace',
324
+ }));
325
+ out.push({
326
+ verb: 'ask',
327
+ why: 'agent:message writes no row; the agent-prompt FALLBACK does — so the count measures '
328
+ + 'stream failures, not usage',
329
+ });
330
+ return out.sort((a, b) => a.verb.localeCompare(b.verb));
331
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The sha256 an update manifest commits to, or null when it doesn't
3
+ * commit to one.
4
+ *
5
+ * `null` means DO NOT AUTO-UPDATE. The daemon used to execute
6
+ * `curl … | sh` on an hourly timer as soon as `latest.json` advertised a
7
+ * newer version — no digest, no signature, no prompt — so whatever the
8
+ * bucket served became code on every machine. An update that cannot be
9
+ * verified is now simply not installed; the user is told to run the
10
+ * installer deliberately instead.
11
+ *
12
+ * Strict on purpose: a truncated, upper-case-mixed or non-hex value is
13
+ * treated as absent rather than coerced, because "almost a digest" must
14
+ * never satisfy a check whose entire job is exactness.
15
+ */
16
+ export function usableUpdateDigest(manifest) {
17
+ if (!manifest || typeof manifest !== 'object')
18
+ return null;
19
+ const raw = manifest.sha256;
20
+ if (typeof raw !== 'string')
21
+ return null;
22
+ const lowered = raw.trim().toLowerCase();
23
+ return /^[0-9a-f]{64}$/.test(lowered) ? lowered : null;
24
+ }
25
+ /**
26
+ * Compare two semver strings: >0 if a > b, <0 if a < b, 0 if equal.
27
+ *
28
+ * ⚠⚠ IT COULD NOT READ A PRERELEASE, AND THE FAILURE WAS SILENT AND BACKWARDS.
29
+ * `'0.7.7-rc.1'.split('.')` yields `['0','7','7-rc','1']`, `Number('7-rc')` is NaN, and `NaN || 0`
30
+ * is 0 — so a release candidate parsed as **0.7.0**. The consequences all point the wrong way:
31
+ * `rc.2` and `rc.1` compared EQUAL so an RC could never update to a later RC, and `0.8.0-rc.1`
32
+ * compared equal to the finished `0.8.0` so an RC machine would never come back to stable. This
33
+ * function gates `checkAndAutoUpdate`, so "equal" means "do nothing, forever".
34
+ *
35
+ * ⚠ A PRERELEASE IS LESS THAN ITS RELEASE — that is the whole point of the notation, and it is the
36
+ * rule that lets an RC upgrade INTO the stable build of the same number. Handled explicitly,
37
+ * because numeric comparison alone cannot express it: `1.0.0-rc.1` and `1.0.0` have identical
38
+ * numeric parts.
39
+ *
40
+ * ⚠ IDENTIFIERS COMPARE NUMERICALLY WHEN BOTH ARE NUMERIC, otherwise as strings — so `rc.10` sorts
41
+ * after `rc.9` rather than before it, which a plain string compare gets wrong.
42
+ *
43
+ * ⚠ THERE WAS A SECOND COPY of this in `first-party-hives.ts` that stripped the prerelease with
44
+ * `split('-')[0]` — different behaviour, same name, and neither knew about the other. It imports
45
+ * this one now.
46
+ */
47
+ export function compareVersions(a, b) {
48
+ const split = (v) => {
49
+ const [core, ...rest] = String(v).trim().split('-');
50
+ return { nums: core.split('.').map((n) => Number(n) || 0), pre: rest.join('-') };
51
+ };
52
+ const x = split(a), y = split(b);
53
+ for (let i = 0; i < 3; i++) {
54
+ const diff = (x.nums[i] || 0) - (y.nums[i] || 0);
55
+ if (diff !== 0)
56
+ return diff;
57
+ }
58
+ // Numeric parts equal: absence of a prerelease wins.
59
+ if (!x.pre && !y.pre)
60
+ return 0;
61
+ if (!x.pre)
62
+ return 1;
63
+ if (!y.pre)
64
+ return -1;
65
+ const xi = x.pre.split('.'), yi = y.pre.split('.');
66
+ for (let i = 0; i < Math.max(xi.length, yi.length); i++) {
67
+ const p = xi[i], q = yi[i];
68
+ if (p === undefined)
69
+ return -1; // a shorter set of identifiers has lower precedence
70
+ if (q === undefined)
71
+ return 1;
72
+ const pn = /^\d+$/.test(p), qn = /^\d+$/.test(q);
73
+ if (pn && qn) {
74
+ const d = Number(p) - Number(q);
75
+ if (d !== 0)
76
+ return d;
77
+ continue;
78
+ }
79
+ if (pn !== qn)
80
+ return pn ? -1 : 1; // numeric identifiers are lower than alphanumeric ones
81
+ if (p !== q)
82
+ return p < q ? -1 : 1;
83
+ }
84
+ return 0;
85
+ }
@@ -0,0 +1,207 @@
1
+ // ============================================================================
2
+ // Beehaven Office - Claude Code Event Watcher
3
+ // Primary: Unix domain socket HTTP server at /tmp/beehaven.sock
4
+ // Fallback: Watches the JSONL events file written by the hook script
5
+ // ============================================================================
6
+ // chokidar is imported lazily so a missing bundle (e.g. when the daemon ships
7
+ // inside an Electron app whose node_modules layout doesn't include chokidar)
8
+ // degrades the file-watcher to a no-op rather than crashing the whole daemon.
9
+ // The primary Unix-socket path does not depend on chokidar.
10
+ import { createReadStream, existsSync, writeFileSync, statSync, renameSync, unlinkSync, chmodSync, watch as fsWatch, } from 'fs';
11
+ import { createInterface } from 'readline';
12
+ import { createServer } from 'http';
13
+ import { EventEmitter } from 'events';
14
+ const EVENTS_FILE = '/tmp/beehaven-events.jsonl';
15
+ const SOCKET_PATH = '/tmp/beehaven.sock';
16
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
17
+ export { SOCKET_PATH };
18
+ export class ClaudeWatcher extends EventEmitter {
19
+ lastSize = 0;
20
+ processing = false;
21
+ fsWatcher = null;
22
+ socketServer = null;
23
+ start() {
24
+ this.startSocket();
25
+ this.startFileWatcher();
26
+ }
27
+ /** Stop watching for events */
28
+ stop() {
29
+ this.stopSocket();
30
+ this.stopFileWatcher();
31
+ }
32
+ // ── Unix Domain Socket ───────────────────────────────────────────────
33
+ /** Start HTTP server on Unix domain socket for direct event delivery */
34
+ startSocket() {
35
+ // Remove stale socket file
36
+ try {
37
+ if (existsSync(SOCKET_PATH))
38
+ unlinkSync(SOCKET_PATH);
39
+ }
40
+ catch {
41
+ /* may not exist */
42
+ }
43
+ this.socketServer = createServer((req, res) => {
44
+ if (req.method !== 'POST' || req.url !== '/event') {
45
+ res.writeHead(404);
46
+ res.end();
47
+ return;
48
+ }
49
+ const chunks = [];
50
+ req.on('data', (chunk) => chunks.push(chunk));
51
+ req.on('end', () => {
52
+ try {
53
+ const body = Buffer.concat(chunks).toString('utf8');
54
+ const event = JSON.parse(body);
55
+ this.emit('event', event);
56
+ res.writeHead(200, { 'Content-Type': 'application/json' });
57
+ res.end('{"ok":true}');
58
+ }
59
+ catch {
60
+ res.writeHead(400);
61
+ res.end('{"error":"invalid json"}');
62
+ }
63
+ });
64
+ });
65
+ this.socketServer.on('error', (err) => {
66
+ console.warn(`[watcher] Socket server error: ${err.message}`);
67
+ });
68
+ // Bind first, then chmod — changing process.umask is global and
69
+ // races with any other mkdirSync / writeFileSync happening in
70
+ // parallel (e.g. LocalHive creating per-building SQLite dirs
71
+ // during startup). A post-bind chmod achieves the same security
72
+ // without the race.
73
+ this.socketServer.listen(SOCKET_PATH, () => {
74
+ try {
75
+ chmodSync(SOCKET_PATH, 0o600);
76
+ }
77
+ catch { }
78
+ console.log(`[watcher] Socket listening on ${SOCKET_PATH}`);
79
+ });
80
+ }
81
+ /** Stop the Unix domain socket server */
82
+ stopSocket() {
83
+ if (this.socketServer) {
84
+ this.socketServer.close();
85
+ this.socketServer = null;
86
+ }
87
+ try {
88
+ if (existsSync(SOCKET_PATH))
89
+ unlinkSync(SOCKET_PATH);
90
+ }
91
+ catch {
92
+ /* may already be gone */
93
+ }
94
+ }
95
+ // ── File Watcher (fallback) ──────────────────────────────────────────
96
+ /** Start file watcher as fallback for when socket isn't available */
97
+ async startFileWatcher() {
98
+ // Create events file if it doesn't exist
99
+ if (!existsSync(EVENTS_FILE)) {
100
+ writeFileSync(EVENTS_FILE, '', { mode: 0o600 });
101
+ }
102
+ // Get initial file size
103
+ this.lastSize = statSync(EVENTS_FILE).size;
104
+ // Try chokidar (preferred: usePolling catches atomic writes). If it's
105
+ // not bundled with this daemon — e.g. running inside an Electron .app
106
+ // whose node_modules layout doesn't ship it — fall back to Node's
107
+ // built-in fs.watch. The daemon's IPC socket does not depend on this.
108
+ try {
109
+ const { watch } = await import('chokidar');
110
+ this.fsWatcher = watch(EVENTS_FILE, {
111
+ persistent: true,
112
+ usePolling: true,
113
+ interval: 100,
114
+ });
115
+ this.fsWatcher.on('change', () => {
116
+ this.readNewLines();
117
+ });
118
+ console.log(`[watcher] File watcher on ${EVENTS_FILE} (chokidar)`);
119
+ }
120
+ catch (err) {
121
+ // chokidar missing — use fs.watch instead. Less robust against
122
+ // editor atomic-write patterns, but good enough for a JSONL that
123
+ // is only ever appended to by the hook script.
124
+ try {
125
+ this.fsWatcher = fsWatch(EVENTS_FILE, { persistent: true }, () => {
126
+ this.readNewLines();
127
+ });
128
+ console.log(`[watcher] File watcher on ${EVENTS_FILE} (fs.watch fallback — chokidar not bundled)`);
129
+ }
130
+ catch (e2) {
131
+ console.warn(`[watcher] Could not start file watcher: ${e2.message}`);
132
+ this.fsWatcher = null;
133
+ }
134
+ }
135
+ // Rotate oversized file on startup
136
+ this.rotateIfNeeded();
137
+ }
138
+ /** Stop file watcher */
139
+ stopFileWatcher() {
140
+ if (this.fsWatcher) {
141
+ this.fsWatcher.close();
142
+ this.fsWatcher = null;
143
+ }
144
+ }
145
+ // ── File rotation ────────────────────────────────────────────────────
146
+ /** Rotate the events file if it exceeds MAX_FILE_SIZE */
147
+ rotateIfNeeded() {
148
+ try {
149
+ const size = statSync(EVENTS_FILE).size;
150
+ if (size > MAX_FILE_SIZE) {
151
+ const rotated = `${EVENTS_FILE}.${Date.now()}.old`;
152
+ renameSync(EVENTS_FILE, rotated);
153
+ chmodSync(rotated, 0o600);
154
+ writeFileSync(EVENTS_FILE, '', { mode: 0o600 });
155
+ this.lastSize = 0;
156
+ console.log(`[watcher] Rotated events file (${(size / 1024 / 1024).toFixed(1)} MB) → ${rotated}`);
157
+ }
158
+ }
159
+ catch {
160
+ // File may not exist yet
161
+ }
162
+ }
163
+ async readNewLines() {
164
+ if (this.processing)
165
+ return;
166
+ this.processing = true;
167
+ try {
168
+ const currentSize = statSync(EVENTS_FILE).size;
169
+ if (currentSize <= this.lastSize) {
170
+ // File was truncated or unchanged
171
+ if (currentSize < this.lastSize)
172
+ this.lastSize = 0;
173
+ this.processing = false;
174
+ return;
175
+ }
176
+ const stream = createReadStream(EVENTS_FILE, {
177
+ start: this.lastSize,
178
+ encoding: 'utf8',
179
+ });
180
+ const rl = createInterface({ input: stream });
181
+ for await (const line of rl) {
182
+ if (!line.trim())
183
+ continue;
184
+ try {
185
+ const event = JSON.parse(line);
186
+ this.emit('event', event);
187
+ }
188
+ catch (err) {
189
+ console.warn(`[watcher] Skipping malformed JSON line: ${line.slice(0, 100)}`, err.message);
190
+ }
191
+ }
192
+ this.lastSize = currentSize;
193
+ // Rotate if file has grown too large during this session
194
+ this.rotateIfNeeded();
195
+ }
196
+ catch (err) {
197
+ console.warn(`[watcher] File read error (resetting offset):`, err.message);
198
+ this.lastSize = 0;
199
+ }
200
+ this.processing = false;
201
+ }
202
+ /** Clear the events file */
203
+ clear() {
204
+ writeFileSync(EVENTS_FILE, '', { mode: 0o600 });
205
+ this.lastSize = 0;
206
+ }
207
+ }