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.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* beehaven explain <action> — schema + examples for one action.
|
|
3
|
+
*
|
|
4
|
+
* Where `beehaven actions` lists every action name, `explain` renders
|
|
5
|
+
* ONE action's input schema, description, and 2-3 example invocations.
|
|
6
|
+
* Agents call this before `beehaven call <action>` to know what payload
|
|
7
|
+
* shape to send — no more guessing at field names.
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* 1. Try `describe-action` (home Hive exposes this); if the target
|
|
11
|
+
* has it, the response includes schema + description + examples.
|
|
12
|
+
* 2. If the target doesn't have describe-action (unknown-action error),
|
|
13
|
+
* fall back to `list-actions --format=full` and filter client-side.
|
|
14
|
+
* Every DO with list-actions + the right format flag gives us the
|
|
15
|
+
* same data.
|
|
16
|
+
* 3. If BOTH fail, suggest `beehaven cat /.workspace.json` — the raw
|
|
17
|
+
* manifest always carries action schemas.
|
|
18
|
+
*
|
|
19
|
+
* Routing: honors the CLI-side target (same as `call` / `actions`).
|
|
20
|
+
* Explicit --building wins, then file-based target, then legacy daemon
|
|
21
|
+
* session fallback.
|
|
22
|
+
*/
|
|
23
|
+
import { rpcOnce } from '../ipc-client.js';
|
|
24
|
+
/** Same logic as workspace.ts:getTimeout — inlined to avoid importing the full module. */
|
|
25
|
+
function resolveTimeout(values) {
|
|
26
|
+
if (values.timeout) {
|
|
27
|
+
const n = parseInt(values.timeout, 10);
|
|
28
|
+
if (Number.isFinite(n) && n > 0)
|
|
29
|
+
return n;
|
|
30
|
+
}
|
|
31
|
+
return 30_000;
|
|
32
|
+
}
|
|
33
|
+
// ── Entry ──────────────────────────────────────────────────────────────
|
|
34
|
+
export async function cmdExplain(actionName, values = {}) {
|
|
35
|
+
if (!actionName) {
|
|
36
|
+
console.error(' Usage: beehaven explain <action>');
|
|
37
|
+
console.error(' Example: beehaven explain note-create');
|
|
38
|
+
console.error('');
|
|
39
|
+
console.error(' Tip: `beehaven actions` to list every action on the current target.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
if (!values.json) {
|
|
43
|
+
const target = values.building ? values.building : 'home';
|
|
44
|
+
console.error(`\x1b[2m→ ${target} describe-action ${actionName}\x1b[0m`);
|
|
45
|
+
}
|
|
46
|
+
const manifest = await fetchManifest(actionName, values);
|
|
47
|
+
if (!manifest) {
|
|
48
|
+
console.error(` No manifest for action "${actionName}".`);
|
|
49
|
+
console.error('');
|
|
50
|
+
console.error(' Try:');
|
|
51
|
+
console.error(' beehaven actions # is this action spelled right?');
|
|
52
|
+
console.error(' beehaven actions --search ' + actionName);
|
|
53
|
+
console.error(' beehaven cat /.workspace.json # raw manifest (always available)');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
if (values.json) {
|
|
57
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
render(manifest);
|
|
61
|
+
}
|
|
62
|
+
// ── Fetch ──────────────────────────────────────────────────────────────
|
|
63
|
+
async function fetchManifest(name, values) {
|
|
64
|
+
const timeout = resolveTimeout(values);
|
|
65
|
+
// Strategy 1: describe-action if available (home does).
|
|
66
|
+
try {
|
|
67
|
+
const { resolveBuildingForExplain } = await import('./explain-router.js');
|
|
68
|
+
const resolved = resolveBuildingForExplain('describe-action', { name }, values.building);
|
|
69
|
+
const raw = await rpcOnce(resolved.action, resolved.input, timeout);
|
|
70
|
+
const inner = unwrap(raw);
|
|
71
|
+
if (inner && !inner.error && (inner.name || inner.input || inner.description)) {
|
|
72
|
+
const m = normalize(inner);
|
|
73
|
+
return await enrichAlias(m, name, values, timeout);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// fall through to strategy 2
|
|
78
|
+
}
|
|
79
|
+
// Strategy 2: list-actions --format=full + filter.
|
|
80
|
+
try {
|
|
81
|
+
const { resolveBuildingForExplain } = await import('./explain-router.js');
|
|
82
|
+
const resolved = resolveBuildingForExplain('list-actions', { format: 'full' }, values.building);
|
|
83
|
+
const raw = await rpcOnce(resolved.action, resolved.input, timeout);
|
|
84
|
+
const inner = unwrap(raw);
|
|
85
|
+
const actions = inner?.actions ?? (Array.isArray(inner) ? inner : []);
|
|
86
|
+
const match = actions.find((a) => a.name === name);
|
|
87
|
+
if (match)
|
|
88
|
+
return normalize(match);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// fall through
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* ⚠⚠ AN ALIAS ANSWERS WITH A DIFFERENT SCHEMA THAN ITS CANONICAL NAME, SO ASK FOR BOTH (CBH2-18).
|
|
97
|
+
*
|
|
98
|
+
* `beehaven actions` LISTS actions under their alias names — `velocity` and `burndown` appear,
|
|
99
|
+
* `board-report` does not — so the name an agent discovers is exactly the name whose schema is
|
|
100
|
+
* degraded. One extra round trip on the rare alias path is worth strictly more than a correct
|
|
101
|
+
* answer that only the undiscoverable name can produce.
|
|
102
|
+
*
|
|
103
|
+
* ⚠ ONE EXTRA FETCH, AND ONLY ON AN ALIAS. `isAlias` comes from the server, so the common path
|
|
104
|
+
* (canonical name) costs nothing and this cannot recurse: the second call asks for the canonical
|
|
105
|
+
* name, whose own reply has `isAlias: false`.
|
|
106
|
+
*
|
|
107
|
+
* ⚠ NON-FATAL BY CONSTRUCTION. Enrichment is a bonus; a daemon hiccup on the second call must
|
|
108
|
+
* degrade to the alias's own manifest, never to an error. `explain` failing because the *optional*
|
|
109
|
+
* half of its answer failed would be strictly worse than the bug being fixed.
|
|
110
|
+
*/
|
|
111
|
+
async function enrichAlias(m, requestedName, values, timeout) {
|
|
112
|
+
if (!m.isAlias || !m.name || m.name === requestedName)
|
|
113
|
+
return m;
|
|
114
|
+
try {
|
|
115
|
+
const { resolveBuildingForExplain } = await import('./explain-router.js');
|
|
116
|
+
const resolved = resolveBuildingForExplain('describe-action', { name: m.name }, values.building);
|
|
117
|
+
const raw = await rpcOnce(resolved.action, resolved.input, timeout);
|
|
118
|
+
const inner = unwrap(raw);
|
|
119
|
+
if (!inner || inner.error)
|
|
120
|
+
return m;
|
|
121
|
+
const canonical = normalize(inner);
|
|
122
|
+
return {
|
|
123
|
+
...m,
|
|
124
|
+
input: mergeInputSchemas(m.input, canonical.input),
|
|
125
|
+
// The canonical registration is where a curated description lives, too.
|
|
126
|
+
description: m.description || canonical.description,
|
|
127
|
+
examples: m.examples ?? canonical.examples,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return m;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function unwrap(raw) {
|
|
135
|
+
if (!raw)
|
|
136
|
+
return null;
|
|
137
|
+
if (raw.type === 'ack' && raw.result !== undefined)
|
|
138
|
+
return raw.result;
|
|
139
|
+
if (raw.result !== undefined)
|
|
140
|
+
return raw.result;
|
|
141
|
+
return raw;
|
|
142
|
+
}
|
|
143
|
+
function normalize(raw) {
|
|
144
|
+
return {
|
|
145
|
+
name: raw.name ?? '',
|
|
146
|
+
description: raw.description,
|
|
147
|
+
category: raw.category,
|
|
148
|
+
aliases: Array.isArray(raw.aliases) ? raw.aliases : undefined,
|
|
149
|
+
input: raw.input ?? raw.inputSchema ?? raw.schema,
|
|
150
|
+
output: raw.output ?? raw.outputSchema,
|
|
151
|
+
examples: Array.isArray(raw.examples) ? raw.examples : undefined,
|
|
152
|
+
requested: typeof raw.requested === 'string' ? raw.requested : undefined,
|
|
153
|
+
isAlias: raw.isAlias === true ? true : undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* ⚠⚠ ONE ACTION, TWO PARTIAL SCHEMAS — MERGED PER PROPERTY, NOT PER ACTION (CBH2-18).
|
|
158
|
+
*
|
|
159
|
+
* `board-report` is reachable under four names, and the server answers each with a DIFFERENT
|
|
160
|
+
* subset of one schema. Measured live on staging:
|
|
161
|
+
*
|
|
162
|
+
* explain board-report → kind: { type:'string', description:'velocity | burndown | …' }
|
|
163
|
+
* explain velocity → kind: { type:'string', enum:['velocity','burndown',…] }
|
|
164
|
+
*
|
|
165
|
+
* So it is NOT simply that the alias is degraded — each NAME carries something the other lacks.
|
|
166
|
+
* The canonical name has the curated prose; the alias has the derived `enum`. A fix that merely
|
|
167
|
+
* preferred the canonical would recover the descriptions and still throw the enum away, i.e. fix
|
|
168
|
+
* the ticket and keep half the bug.
|
|
169
|
+
*
|
|
170
|
+
* ⚠ THE MERGE IS PER PROPERTY KEY, and that is the whole point. A whole-object
|
|
171
|
+
* `{...canonical, ...alias}` lets one side's `{type:'string'}` shadow the other side's
|
|
172
|
+
* `description` — the exact per-action-vs-per-field mistake `ACTION_INPUT_SCHEMAS` records making
|
|
173
|
+
* on `sheet-get`, where curation won outright and hid two working fields.
|
|
174
|
+
*
|
|
175
|
+
* ⚠ `required` IS UNIONED. Neither side is a superset: curation marks what it knows, the
|
|
176
|
+
* derivation marks what each handler's own "<field> is required" refusal proves.
|
|
177
|
+
*
|
|
178
|
+
* ⚠ NOT A GUESS WHEN EITHER SIDE IS NOT A JSON SCHEMA. A raw Zod descriptor map has no
|
|
179
|
+
* `properties`, so there is no key-wise merge to do; return the richer side untouched rather than
|
|
180
|
+
* inventing a shape neither server sent.
|
|
181
|
+
*/
|
|
182
|
+
function mergeInputSchemas(requested, canonical) {
|
|
183
|
+
const isObj = (v) => !!v && typeof v === 'object';
|
|
184
|
+
if (!isObj(requested))
|
|
185
|
+
return canonical;
|
|
186
|
+
if (!isObj(canonical))
|
|
187
|
+
return requested;
|
|
188
|
+
const rp = requested.properties;
|
|
189
|
+
const cp = canonical.properties;
|
|
190
|
+
// One of them is not a JSON Schema (raw Zod map / field map) — do not invent a union.
|
|
191
|
+
if (!isObj(rp) || !isObj(cp))
|
|
192
|
+
return isObj(rp) ? requested : canonical;
|
|
193
|
+
const properties = {};
|
|
194
|
+
for (const key of new Set([...Object.keys(cp), ...Object.keys(rp)])) {
|
|
195
|
+
const rv = rp[key];
|
|
196
|
+
const cv = cp[key];
|
|
197
|
+
if (!isObj(rv) || !isObj(cv)) {
|
|
198
|
+
properties[key] = rv ?? cv;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// Requested wins on shared keys (it is the derived, complete-from-the-handler side)…
|
|
202
|
+
const merged = { ...cv, ...rv };
|
|
203
|
+
// …EXCEPT the description, which is the curated half and the reason to merge at all.
|
|
204
|
+
const prose = [cv?.description, rv?.description].find((d) => typeof d === 'string' && d.trim() !== '');
|
|
205
|
+
if (prose)
|
|
206
|
+
merged.description = prose;
|
|
207
|
+
else
|
|
208
|
+
delete merged.description;
|
|
209
|
+
properties[key] = merged;
|
|
210
|
+
}
|
|
211
|
+
const required = [
|
|
212
|
+
...new Set([
|
|
213
|
+
...(Array.isArray(canonical.required) ? canonical.required : []),
|
|
214
|
+
...(Array.isArray(requested.required) ? requested.required : []),
|
|
215
|
+
]),
|
|
216
|
+
];
|
|
217
|
+
return {
|
|
218
|
+
...canonical,
|
|
219
|
+
...requested,
|
|
220
|
+
properties,
|
|
221
|
+
...(required.length ? { required } : {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// ── Render ─────────────────────────────────────────────────────────────
|
|
225
|
+
function render(m) {
|
|
226
|
+
console.log('');
|
|
227
|
+
console.log(` ${bold(m.name)}${m.category ? dim(` [${m.category}]`) : ''}`);
|
|
228
|
+
if (m.description) {
|
|
229
|
+
console.log(indent(m.description));
|
|
230
|
+
}
|
|
231
|
+
console.log('');
|
|
232
|
+
// Aliases — quick scan row for agents
|
|
233
|
+
if (m.aliases && m.aliases.length > 0) {
|
|
234
|
+
console.log(` ${bold('Also called')} ${m.aliases.join(', ')}`);
|
|
235
|
+
console.log('');
|
|
236
|
+
}
|
|
237
|
+
// Input schema.
|
|
238
|
+
//
|
|
239
|
+
// ABSENCE IS NOT EMPTINESS. This printed "none (empty object)" whenever the manifest carried
|
|
240
|
+
// no schema — a positive claim that the action takes no arguments. 731 of 802 actions on the
|
|
241
|
+
// workspace register without a zod input (`defineApp(name, { run })` is the common form), so
|
|
242
|
+
// the CLI was telling an agent "this takes nothing" about 91% of the surface, including
|
|
243
|
+
// `ticket-create`, which requires projectId AND title.
|
|
244
|
+
//
|
|
245
|
+
// The server is not at fault: `describe-action` returns `input: null` for undeclared and a
|
|
246
|
+
// real object for declared. Only the renderer conflated them.
|
|
247
|
+
if (m.input && Object.keys(m.input).length > 0) {
|
|
248
|
+
console.log(` ${bold('Input')}`);
|
|
249
|
+
renderSchema(m.input);
|
|
250
|
+
console.log('');
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
console.log(` ${bold('Input')} ${dim('not declared — the description above carries the shape')}`);
|
|
254
|
+
console.log('');
|
|
255
|
+
}
|
|
256
|
+
// Output schema
|
|
257
|
+
if (m.output && Object.keys(m.output).length > 0) {
|
|
258
|
+
console.log(` ${bold('Output')}`);
|
|
259
|
+
renderSchema(m.output);
|
|
260
|
+
console.log('');
|
|
261
|
+
}
|
|
262
|
+
// Examples
|
|
263
|
+
if (m.examples && m.examples.length > 0) {
|
|
264
|
+
console.log(` ${bold('Examples')}`);
|
|
265
|
+
for (const ex of m.examples) {
|
|
266
|
+
if (typeof ex === 'string') {
|
|
267
|
+
console.log(` beehaven call ${m.name} '${ex}'`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const input = typeof ex.input === 'string' ? ex.input : JSON.stringify(ex.input);
|
|
271
|
+
console.log(` beehaven call ${m.name} '${input}'`);
|
|
272
|
+
if (ex.note)
|
|
273
|
+
console.log(` ${dim('# ' + ex.note)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
console.log('');
|
|
277
|
+
}
|
|
278
|
+
else if (m.input && Object.keys(m.input).length > 0) {
|
|
279
|
+
// Synthesize a minimal example — but ONLY from a real schema. With no schema this
|
|
280
|
+
// synthesized `{}`, printing `beehaven call ticket-create '{}'` under the heading
|
|
281
|
+
// "Example": a copy-pasteable line that is guaranteed to fail. A fabricated example is
|
|
282
|
+
// worse than none, because it reads as verified.
|
|
283
|
+
const minimal = synthMinimalInput(m.input);
|
|
284
|
+
/**
|
|
285
|
+
* ⚠ AN EMPTY SYNTHESIS MEANS "we could not tell which fields are required" — print NOTHING
|
|
286
|
+
* rather than `'{}'`. `beehaven call ticket-create '{}'` under a heading that says "Example"
|
|
287
|
+
* is a copy-pasteable line guaranteed to fail, which is the case this block's own comment
|
|
288
|
+
* above was written for. The schema is rendered directly above; that is the honest answer.
|
|
289
|
+
*/
|
|
290
|
+
if (Object.keys(minimal).length > 0) {
|
|
291
|
+
for (const line of exampleBlock(m.name, minimal))
|
|
292
|
+
console.log(line);
|
|
293
|
+
console.log('');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
console.log(` ${dim('Call it:')} beehaven call ${m.name} '<json>'`);
|
|
297
|
+
console.log('');
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The pure half of `renderSchema`: schema in, rows out. `null` means "not a field map" — the two
|
|
301
|
+
* degenerate cases the caller prints as `(opaque schema)` / the raw JSON.
|
|
302
|
+
*/
|
|
303
|
+
function schemaRows(schema) {
|
|
304
|
+
if (!schema || typeof schema !== 'object')
|
|
305
|
+
return null;
|
|
306
|
+
// Zod-like nested: { shape: {...}, _def: {...} }
|
|
307
|
+
const fields = schema.shape ?? schema.properties ?? schema;
|
|
308
|
+
if (typeof fields !== 'object' || fields === null)
|
|
309
|
+
return null;
|
|
310
|
+
const entries = Object.entries(fields);
|
|
311
|
+
/**
|
|
312
|
+
* ⚠⚠ `required` LIVES ON THE SCHEMA, NOT ON THE FIELD — AND THIS DROPPED IT ENTIRELY (CBH2-11).
|
|
313
|
+
*
|
|
314
|
+
* `describeField` decides requiredness from `raw.optional`, which is the shape of our own
|
|
315
|
+
* `FieldSchema` (`{ type: 'string', optional: true }`). JSON Schema does not mark fields at all;
|
|
316
|
+
* it lists required NAMES in a sibling array. So for every JSON-Schema action `raw.optional` was
|
|
317
|
+
* `undefined`, `!undefined` was `true`, and the "optional" label never printed — every field
|
|
318
|
+
* rendered identically whether it was mandatory or not.
|
|
319
|
+
*
|
|
320
|
+
* ⚠ THE SERVER WAS SENDING IT THE WHOLE TIME. `describe-action { name: 'sprint-create' }`
|
|
321
|
+
* returns `required: ['boardId']`; only the renderer discarded it. And 614 fields across the
|
|
322
|
+
* derived map were promoted to `required` by reading each handler's own "<field> is required"
|
|
323
|
+
* refusal — all of that work was invisible at the one door agents are told to use.
|
|
324
|
+
*
|
|
325
|
+
* ⚠ MARKED POSITIVELY ("required"), NOT BY ABSENCE. With a JSON Schema the required set is the
|
|
326
|
+
* small, actionable one, and the old rendering made an unmarked field read as mandatory — the
|
|
327
|
+
* dangerous direction, because a caller who believes everything is required supplies junk for
|
|
328
|
+
* fields they should have omitted.
|
|
329
|
+
*
|
|
330
|
+
* ⚠ BOTH SHAPES STILL WORK: a sibling `required[]` wins where present; otherwise the per-field
|
|
331
|
+
* `optional` flag is honoured exactly as before, so nothing that rendered correctly changes.
|
|
332
|
+
*/
|
|
333
|
+
const requiredNames = Array.isArray(schema?.required)
|
|
334
|
+
? new Set(schema.required.map(String))
|
|
335
|
+
: null;
|
|
336
|
+
const alts = aliasGroups(schema);
|
|
337
|
+
return entries.map(([name, desc]) => {
|
|
338
|
+
const { type, required, hint } = describeField(desc);
|
|
339
|
+
/**
|
|
340
|
+
* ⚠⚠ THE ALIAS `required[]` CANNOT NAME (CBH2-91). 60 actions open
|
|
341
|
+
* `const compId = input.compositionId || input.id;` and refuse with the bare
|
|
342
|
+
* `'compositionId required'` — the refusal names ONE field, the code accepts EITHER. So `id`
|
|
343
|
+
* rendered as plain `optional` beside a required `compositionId`, which is not wrong about
|
|
344
|
+
* `compositionId` and says nothing at all about the alias.
|
|
345
|
+
*
|
|
346
|
+
* ⚠ THE ROW'S required/optional COLUMN IS UNTOUCHED, and that is deliberate. The deriver keeps
|
|
347
|
+
* `compositionId` in `required[]` because following it always produces a working call; a
|
|
348
|
+
* suppression that marked NEITHER was built, measured on 47 actions and removed. What was
|
|
349
|
+
* missing was only that `id` works too, and a hint is where that belongs — a third value in
|
|
350
|
+
* the required column would also overflow the 12-char pad every other row is aligned to.
|
|
351
|
+
*/
|
|
352
|
+
const others = altsFor(alts, name);
|
|
353
|
+
const aliasHint = others.length
|
|
354
|
+
? `${others.length === 1 ? 'or' : 'or one of'} \`${others.join('`, `')}\``
|
|
355
|
+
: null;
|
|
356
|
+
return {
|
|
357
|
+
name,
|
|
358
|
+
type,
|
|
359
|
+
required: requiredNames ? requiredNames.has(name) : required,
|
|
360
|
+
hint: [hint, aliasHint].filter(Boolean).join(' ') || undefined,
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The alias disjunctions a schema declares: each group is a set of field names of which any one
|
|
366
|
+
* satisfies the same slot.
|
|
367
|
+
*
|
|
368
|
+
* ⚠ BOTH ENCODINGS. One disjunction is a top-level `anyOf`; several INDEPENDENT ones are an
|
|
369
|
+
* `allOf` of `anyOf`s, because flattening two groups into one `anyOf` would read as "any one of
|
|
370
|
+
* these five fields", a different and false claim. The deriver emits whichever fits; a reader that
|
|
371
|
+
* knew only `anyOf` would silently see nothing on `session-note-add`.
|
|
372
|
+
*/
|
|
373
|
+
function aliasGroups(schema) {
|
|
374
|
+
const groups = [];
|
|
375
|
+
const take = (anyOf) => {
|
|
376
|
+
if (!Array.isArray(anyOf))
|
|
377
|
+
return;
|
|
378
|
+
const names = anyOf
|
|
379
|
+
.map((b) => (Array.isArray(b?.required) && b.required.length === 1 ? String(b.required[0]) : null))
|
|
380
|
+
.filter((n) => !!n);
|
|
381
|
+
if (names.length > 1)
|
|
382
|
+
groups.push(names);
|
|
383
|
+
};
|
|
384
|
+
take(schema?.anyOf);
|
|
385
|
+
if (Array.isArray(schema?.allOf))
|
|
386
|
+
for (const part of schema.allOf)
|
|
387
|
+
take(part?.anyOf);
|
|
388
|
+
return groups;
|
|
389
|
+
}
|
|
390
|
+
function altsFor(groups, name) {
|
|
391
|
+
const out = new Set();
|
|
392
|
+
for (const g of groups)
|
|
393
|
+
if (g.includes(name))
|
|
394
|
+
for (const n of g)
|
|
395
|
+
if (n !== name)
|
|
396
|
+
out.add(n);
|
|
397
|
+
return [...out];
|
|
398
|
+
}
|
|
399
|
+
/** The formatting half: rows in, printable lines out (padding + colour, no I/O). */
|
|
400
|
+
function formatSchemaRows(rows, indent = ' ') {
|
|
401
|
+
if (rows.length === 0)
|
|
402
|
+
return [`${indent}${dim('(empty)')}`];
|
|
403
|
+
const maxNameW = Math.min(30, Math.max(...rows.map((r) => r.name.length)));
|
|
404
|
+
return rows.map((r) => {
|
|
405
|
+
const nameCol = r.name.padEnd(maxNameW + 2);
|
|
406
|
+
const typeCol = r.type.padEnd(12);
|
|
407
|
+
/**
|
|
408
|
+
* ⚠ PAD THE VISIBLE TEXT, THEN COLOUR IT. `dim()` wraps the string in ANSI escapes, which
|
|
409
|
+
* `padEnd` counts as characters — so padding the coloured string mis-aligns every dimmed cell
|
|
410
|
+
* against every undimmed one. Width first, colour second.
|
|
411
|
+
*/
|
|
412
|
+
/**
|
|
413
|
+
* ⚠ MARK BOTH WAYS ALWAYS. The fallback branch printed '' for required, so an action with a
|
|
414
|
+
* `required[]` said "required" and one without said nothing — two renderings of the same fact,
|
|
415
|
+
* and the blank one is indistinguishable from "we don't know".
|
|
416
|
+
*/
|
|
417
|
+
const reqText = r.required ? 'required' : 'optional';
|
|
418
|
+
const padded = (' ' + reqText).padEnd(12);
|
|
419
|
+
const reqLabel = reqText === 'optional' ? dim(padded) : padded;
|
|
420
|
+
const hintLabel = r.hint ? ` ${dim('# ' + r.hint)}` : '';
|
|
421
|
+
return `${indent}${nameCol}${typeCol}${reqLabel}${hintLabel}`;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function renderSchema(schema, indent = ' ') {
|
|
425
|
+
const rows = schemaRows(schema);
|
|
426
|
+
if (rows === null) {
|
|
427
|
+
const isObj = !!schema && typeof schema === 'object';
|
|
428
|
+
console.log(`${indent}${dim(isObj ? JSON.stringify(schema) : '(opaque schema)')}`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
for (const line of formatSchemaRows(rows, indent))
|
|
432
|
+
console.log(line);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* ⚠⚠ THE IMPLEMENTATION'S TYPE NAMES LEAKED INTO THE PRODUCT (CBH2-31).
|
|
436
|
+
*
|
|
437
|
+
* The server answers "what does this take" in TWO shapes, and the renderer printed each one's
|
|
438
|
+
* vocabulary verbatim — so the same question got two visibly different answers:
|
|
439
|
+
*
|
|
440
|
+
* explain document-edit → id string required # The documentId…
|
|
441
|
+
* explain document-create → title ZodString? optional
|
|
442
|
+
*
|
|
443
|
+
* `ZodString?` is a zod INTERNAL, produced by `_describeInputSchema`'s walk of the registration.
|
|
444
|
+
* A caller reading it learns our validator library, not their argument type — and the trailing `?`
|
|
445
|
+
* duplicates, in a second notation, the requiredness the row already states in words.
|
|
446
|
+
*
|
|
447
|
+
* ⚠ THE `_def` BRANCH WAS ALREADY CORRECT and this is why the split was easy to miss: it does
|
|
448
|
+
* `replace(/^Zod/,'').toLowerCase()`, so it has always emitted `string`. Only the STRING-descriptor
|
|
449
|
+
* branch — the one the live server actually sends — passed the name through untouched.
|
|
450
|
+
*
|
|
451
|
+
* ⚠ ANCHORED ON `Zod` + AN UPPERCASE LETTER, so a user type that merely starts with those three
|
|
452
|
+
* characters is left alone. A blanket `/^Zod/` turns a field typed `Zodiac` into `iac`.
|
|
453
|
+
*
|
|
454
|
+
* ⚠ THE FALLBACK IS STRIP-AND-LOWERCASE, NOT `unknown`. An unmapped zod type still reads better as
|
|
455
|
+
* its own lowercased name (`ZodOptional` → `optional`) than as a word that claims we know nothing.
|
|
456
|
+
*/
|
|
457
|
+
const ZOD_TYPE_ALIASES = {
|
|
458
|
+
bigint: 'number',
|
|
459
|
+
nativeenum: 'enum',
|
|
460
|
+
record: 'object',
|
|
461
|
+
tuple: 'array',
|
|
462
|
+
};
|
|
463
|
+
function normalizeTypeName(raw) {
|
|
464
|
+
const m = /^Zod([A-Z].*)$/.exec(raw);
|
|
465
|
+
if (!m)
|
|
466
|
+
return raw;
|
|
467
|
+
const bare = m[1].toLowerCase();
|
|
468
|
+
return ZOD_TYPE_ALIASES[bare] ?? bare;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* ⚠⚠ THE ENUM WAS IN THE SCHEMA AND ON NEITHER LINE OF THE OUTPUT (CBH2-33).
|
|
472
|
+
*
|
|
473
|
+
* `canvas-align-blocks.axis` arrives as `{ type:'string', enum:['left','center-h',…] }`. The row
|
|
474
|
+
* printed `axis string required` and the Example printed `"axis":"example"` — so the six legal
|
|
475
|
+
* values were dropped from the description AND from the one line built to be pasted. Everything
|
|
476
|
+
* needed to be right was already in hand.
|
|
477
|
+
*/
|
|
478
|
+
function fieldEnum(raw) {
|
|
479
|
+
const e = raw?.enum ?? raw?.enumValues;
|
|
480
|
+
if (Array.isArray(e) && e.length > 0)
|
|
481
|
+
return e.map(String);
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
function describeField(raw) {
|
|
485
|
+
if (typeof raw === 'string') {
|
|
486
|
+
/**
|
|
487
|
+
* ⚠⚠ THE TRAILING `?` IS THE OPTIONALITY MARKER AND THIS DISCARDED IT (CBH2-11 / CBH2-16).
|
|
488
|
+
*
|
|
489
|
+
* The server renders a zod field as the string `"ZodString?"` — `_describeInputSchema` appends
|
|
490
|
+
* `?` for `ZodOptional`/`ZodDefault`. This branch returned `required: true` unconditionally, so
|
|
491
|
+
* every raw-Zod action printed all fields as required and `synthMinimalInput` put all of them
|
|
492
|
+
* in the example. That is why `explain composition-create` still listed eight fields after the
|
|
493
|
+
* first fix: `fieldsDeclareOptionality` saw string descriptors and believed optionality was
|
|
494
|
+
* knowable, while this function could not read it.
|
|
495
|
+
*
|
|
496
|
+
* ⚠ FOUND BY A REVIEWER EXECUTING THE CODE, not by reading it — and it is one root cause under
|
|
497
|
+
* two tickets. The `?` was in the data the whole time.
|
|
498
|
+
*/
|
|
499
|
+
const optional = raw.endsWith('?');
|
|
500
|
+
return { type: normalizeTypeName(optional ? raw.slice(0, -1) : raw), required: !optional };
|
|
501
|
+
}
|
|
502
|
+
if (!raw || typeof raw !== 'object') {
|
|
503
|
+
return { type: 'unknown', required: true };
|
|
504
|
+
}
|
|
505
|
+
// Our FieldSchema shape: { type: 'string', optional: true, default?: ... }
|
|
506
|
+
if (raw.type) {
|
|
507
|
+
const enumValues = fieldEnum(raw);
|
|
508
|
+
return {
|
|
509
|
+
type: normalizeTypeName(String(raw.type)),
|
|
510
|
+
required: !raw.optional,
|
|
511
|
+
// ⚠ PROSE FIRST, ENUM AS THE FALLBACK. A curated description usually spells the options out
|
|
512
|
+
// already ("velocity | burndown | cycle-time | summary"), so printing both is one fact twice.
|
|
513
|
+
hint: raw.description || (enumValues ? enumValues.join(' | ') : undefined),
|
|
514
|
+
enumValues,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
// Zod-ish: { _def: { typeName: 'ZodString' } }
|
|
518
|
+
if (raw._def) {
|
|
519
|
+
const tn = raw._def.typeName ?? 'unknown';
|
|
520
|
+
const type = normalizeTypeName(tn);
|
|
521
|
+
const required = tn !== 'ZodOptional' && tn !== 'ZodDefault';
|
|
522
|
+
return { type, required };
|
|
523
|
+
}
|
|
524
|
+
return { type: 'object', required: true, hint: 'nested — see --json' };
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* ⚠⚠ THE "MINIMAL" EXAMPLE WAS NEITHER MINIMAL NOR RUNNABLE (CBH2-16).
|
|
528
|
+
*
|
|
529
|
+
* Two defects, one root cause shared with the required-field bug above:
|
|
530
|
+
*
|
|
531
|
+
* 1. IT INCLUDED EVERY FIELD. Requiredness came from `describeField().required`, which reads a
|
|
532
|
+
* per-field `optional` flag that JSON Schema does not have — so `!undefined` was `true` and
|
|
533
|
+
* nothing was filtered. `document-create`'s example listed eight fields including `html`,
|
|
534
|
+
* `markdown`, `content`, `body` and `text`, which are MUTUALLY EXCLUSIVE spellings of one
|
|
535
|
+
* argument. Pasting it asks for five bodies at once.
|
|
536
|
+
* 2. UNKNOWN TYPES BECAME `null`. `sampleValue` fell through to `null`, producing
|
|
537
|
+
* `{"title":null,"data":null,…}` — printed under a heading that says "Example", above a line
|
|
538
|
+
* that says "Call it:". Measured in `agent_activity` during this sprint: an agent pasted one
|
|
539
|
+
* and got `unknown kind 'example'` back.
|
|
540
|
+
*
|
|
541
|
+
* ⚠ AN UNRUNNABLE EXAMPLE IS WORSE THAN NO EXAMPLE. No example makes a caller read the schema; a
|
|
542
|
+
* broken one makes them run it, and on a WRITE verb that means a junk artifact in the workspace.
|
|
543
|
+
*/
|
|
544
|
+
function synthMinimalInput(schema) {
|
|
545
|
+
if (!schema || typeof schema !== 'object')
|
|
546
|
+
return {};
|
|
547
|
+
const fields = schema.shape ?? schema.properties ?? schema;
|
|
548
|
+
if (typeof fields !== 'object' || fields === null)
|
|
549
|
+
return {};
|
|
550
|
+
// ⚠ The sibling `required[]` is the real answer when present — see `renderSchema`.
|
|
551
|
+
const requiredNames = Array.isArray(schema.required)
|
|
552
|
+
? new Set(schema.required.map(String))
|
|
553
|
+
: null;
|
|
554
|
+
/**
|
|
555
|
+
* ⚠⚠ NO `required[]` MEANS WE CANNOT TELL — SO DO NOT CLAIM A MINIMAL EXAMPLE.
|
|
556
|
+
*
|
|
557
|
+
* Without it every field looked required, so the "minimal" example listed ALL of them. On
|
|
558
|
+
* `document-create` that is `html`, `markdown`, `content`, `body` AND `text` — five MUTUALLY
|
|
559
|
+
* EXCLUSIVE spellings of one argument, printed as a single copy-pasteable line. That is not a
|
|
560
|
+
* minimal example, it is an incorrect call rendered under a heading that says "Example".
|
|
561
|
+
*
|
|
562
|
+
* ⚠ Returning `{}` here makes the caller fall through to NO example block at all, which is what
|
|
563
|
+
* this file already argues for elsewhere: "A fabricated example is worse than none, because it
|
|
564
|
+
* reads as verified." The schema is printed directly above; it is the honest answer.
|
|
565
|
+
*/
|
|
566
|
+
/**
|
|
567
|
+
* ⚠⚠ THREE CASES, AND COLLAPSING THEM IS A REGRESSION EITHER WAY. A first cut returned `{}`
|
|
568
|
+
* whenever there was no `required[]` — which is right for a JSON Schema and WRONG for our own
|
|
569
|
+
* FieldSchema shape, where requiredness is genuinely knowable from a per-field `optional` flag.
|
|
570
|
+
* It silently removed the example from every FieldSchema action. Caught by the existing suite.
|
|
571
|
+
*/
|
|
572
|
+
const entries = Object.entries(fields);
|
|
573
|
+
const fieldsDeclareOptionality = entries.some(([, d]) => typeof d === 'string' || (d && typeof d === 'object' && 'optional' in d));
|
|
574
|
+
// Neither source of truth: a JSON Schema with no `required[]` and no per-field flags. We cannot
|
|
575
|
+
// tell, so we do not guess — see the caller, which then prints no Example at all.
|
|
576
|
+
if (!requiredNames && !fieldsDeclareOptionality)
|
|
577
|
+
return {};
|
|
578
|
+
const out = {};
|
|
579
|
+
for (const [name, desc] of entries) {
|
|
580
|
+
const { required } = describeField(desc);
|
|
581
|
+
const isRequired = requiredNames ? requiredNames.has(name) : required;
|
|
582
|
+
/**
|
|
583
|
+
* ⚠⚠ AN AUTHOR-SUPPLIED `example` OUTRANKS REQUIREDNESS (CBH2-97).
|
|
584
|
+
*
|
|
585
|
+
* `board-update` and `ticket-update` LIFT a flat call into `updates`, so `updates` is genuinely
|
|
586
|
+
* not required — and dropping it from `required[]` (the honest schema) also dropped it from the
|
|
587
|
+
* example, leaving `'{"id":"example"}'`: a copy-pasteable call that answers `changed: false`.
|
|
588
|
+
* A confident no-op, which is the very class CBH2-57 exists for.
|
|
589
|
+
*
|
|
590
|
+
* A field carrying an explicit `example` is an author saying "put THIS here" — a stronger
|
|
591
|
+
* statement than requiredness, and the only way to show a working shape for a field the schema
|
|
592
|
+
* correctly marks optional. `sampleValue` already prefers `example` over everything else; this
|
|
593
|
+
* is what lets the field reach it.
|
|
594
|
+
*
|
|
595
|
+
* ⚠ ZERO BLAST RADIUS AT THE TIME OF WRITING, MEASURED: 0 of 414 curated properties carried
|
|
596
|
+
* `example`, `default` or `const`, so nothing that prints an Example today changes shape. Only
|
|
597
|
+
* `default`/`const` are deliberately NOT honoured here — a default is what the handler would
|
|
598
|
+
* have used anyway, so putting it in a "minimal" example adds noise rather than meaning.
|
|
599
|
+
*/
|
|
600
|
+
const carriesExample = !!desc && typeof desc === 'object' && 'example' in desc;
|
|
601
|
+
if (!isRequired && !carriesExample)
|
|
602
|
+
continue;
|
|
603
|
+
/**
|
|
604
|
+
* ⚠ THE WHOLE DESCRIPTOR, NOT ITS TYPE. This passed `type` — a lowercase string — so `enum`,
|
|
605
|
+
* `default` and `const` were unreachable from here and every constrained field got the
|
|
606
|
+
* `"example"` placeholder that its own schema forbids (CBH2-33).
|
|
607
|
+
*/
|
|
608
|
+
out[name] = sampleValue(desc);
|
|
609
|
+
}
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* ⚠⚠ THE EXAMPLE WAS WELL-FORMED, RUNNABLE, AND STILL WROTE JUNK INTO THE GALLERY (CBH2-57).
|
|
614
|
+
*
|
|
615
|
+
* CBH2-15/16/33 fixed everything that made the printed line INVALID — the `null` values, the
|
|
616
|
+
* eight-mutually-exclusive-fields "minimal" example, the `"example"` string in a required enum.
|
|
617
|
+
* Swept across all 1,115 derived schemas on 2026-09-06: **591 print an Example and every one of
|
|
618
|
+
* them now satisfies its own schema** — 0 missing-required, 0 illegal enum, 0 type mismatch, 0
|
|
619
|
+
* nulls. The generator is correct.
|
|
620
|
+
*
|
|
621
|
+
* ⚠⚠ AND THE ORIGINAL HARM SURVIVED ALL OF IT, because it was never about validity. The ticket is
|
|
622
|
+
* titled *"prints a runnable example that writes junk into the gallery"*, and a hunter created a
|
|
623
|
+
* real card, `PLA-3517`, by pasting one. `beehaven call composition-create '{"title":"example"}'`
|
|
624
|
+
* is a perfectly valid call that puts a composition named "example" in someone's workspace. Making
|
|
625
|
+
* it valid made it MORE likely to be run, not less.
|
|
626
|
+
*
|
|
627
|
+
* So the fix is at the render, not the generator: a mutating verb's example is presented as a
|
|
628
|
+
* SHAPE, and the line says what running it verbatim would create. The reader keeps the exact call
|
|
629
|
+
* form — which is the whole value of an example to an agent — and loses the invitation.
|
|
630
|
+
*
|
|
631
|
+
* ⚠ IT IS A NAME-SHAPE HEURISTIC AND IT SAYS SO. There is no `mutates: true` on a registration to
|
|
632
|
+
* read, and inventing one for ~1,100 actions is a different ticket. The failure modes are
|
|
633
|
+
* asymmetric and that is what makes a heuristic acceptable here: a false POSITIVE prints a caution
|
|
634
|
+
* on a harmless read (noise), a false NEGATIVE prints an unmarked write (the bug). So the list
|
|
635
|
+
* leans inclusive, and `explain` is a documentation surface where over-cautioning costs nothing.
|
|
636
|
+
*
|
|
637
|
+
* ⚠ MATCHED ON WORD BOUNDARIES INSIDE A HYPHENATED NAME, never as a substring. `document-restore`
|
|
638
|
+
* must match `restore`; `board-report` must NOT match on `report` containing no verb at all, and —
|
|
639
|
+
* the case that actually bites — `note-content-seed` must not be missed because the verb is last
|
|
640
|
+
* while `list-actions` must not be caught because `list` is first. Splitting on `-`/`:` and testing
|
|
641
|
+
* each segment is what makes both come out right.
|
|
642
|
+
*/
|
|
643
|
+
const MUTATING_SEGMENTS = new Set([
|
|
644
|
+
'create', 'new', 'add', 'insert', 'append', 'seed', 'write', 'update', 'patch', 'edit', 'set',
|
|
645
|
+
'rename', 'move', 'copy', 'duplicate', 'delete', 'remove', 'destroy', 'purge', 'empty', 'clear',
|
|
646
|
+
'archive', 'restore', 'revert', 'rollback', 'rewind', 'undo', 'redo', 'send', 'publish', 'post',
|
|
647
|
+
'upload', 'import', 'export', 'generate', 'draw', 'record', 'revoke', 'grant', 'approve',
|
|
648
|
+
'reject', 'decide', 'start', 'stop', 'complete', 'cancel', 'reset', 'repair', 'backfill',
|
|
649
|
+
'migrate', 'sync', 'install', 'uninstall', 'register', 'link', 'unlink', 'invite', 'admit',
|
|
650
|
+
'evict', 'assign', 'toggle', 'pin', 'unpin', 'star', 'snooze', 'mark', 'takeover', 'upgrade',
|
|
651
|
+
'downgrade', 'topup', 'checkout', 'claim', 'compact', 'vacuum', 'reap', 'apply', 'commit',
|
|
652
|
+
/**
|
|
653
|
+
* ⚠ ADDED FROM THE MEASURED "unknown" BUCKET (CBH2-96), each against a real action rather than
|
|
654
|
+
* from imagination: `agent-memory-forget`/`-remember`, `agent-slug-rotate`,
|
|
655
|
+
* `avatar:honey-spend`, `avatar:champion-unlock`, `avatar:accessory-equip`/`-unequip`,
|
|
656
|
+
* `admin:broadcast`, `admin:migration-run`, `_mcp-oauth-finalize`, `agent-mint-token`,
|
|
657
|
+
* `agent-session-end`, `agent-sandbox-exec`, `avatar:bootstrap`. Adding a segment is only safe
|
|
658
|
+
* when it names a verb that WRITES — every entry here moves actions out of "cannot tell" and
|
|
659
|
+
* into a positive claim, so a wrong one is the false-positive noise the list tolerates, but a
|
|
660
|
+
* word that is usually a noun (`report`, `session`) would move reads too.
|
|
661
|
+
*/
|
|
662
|
+
'forget', 'remember', 'rotate', 'spend', 'unlock', 'equip', 'unequip', 'broadcast', 'finalize',
|
|
663
|
+
'mint', 'end', 'bootstrap', 'run', 'exec',
|
|
664
|
+
]);
|
|
665
|
+
/**
|
|
666
|
+
* ⚠⚠ THE THIRD STATE: A NAME THAT CARRIES NO VERB AT ALL (CBH2-96).
|
|
667
|
+
*
|
|
668
|
+
* The classifier was a boolean over `MUTATING_SEGMENTS`, so every name it did not recognise fell
|
|
669
|
+
* into the SAFE bucket — and CBH2-96 names that blind spot exactly: *"a verb whose name carries no
|
|
670
|
+
* verb at all is still invisible to it"*. Measured across all 1,115 derived actions: 468 named a
|
|
671
|
+
* write and **647 did not** — and of those, only 320 name a READ. The other **327, 29% of the
|
|
672
|
+
* surface, named neither and were silently filed as harmless.** The bucket is not a corner:
|
|
673
|
+
* `agent-memory-forget`, `avatar:honey-spend`, `admin:broadcast`, `admin:migration-run`, `Bash`
|
|
674
|
+
* and `MultiEdit` were all in it. After the segments added below it is 500 / 320 / 295.
|
|
675
|
+
*
|
|
676
|
+
* ⚠⚠ THE DECISION, since CBH2-96 asks for one: KEEP THE HEURISTIC, STOP OVERSTATING IT, AND STOP
|
|
677
|
+
* DEFAULTING TO SAFE. A declared `mutates: true` on the registration is the real fix and remains
|
|
678
|
+
* the right one — it would also let `_rpc:catalog` report it and let the CLI's spend gate reason
|
|
679
|
+
* about it — but it is ~1,100 registrations across files this change does not own, and half of it
|
|
680
|
+
* (some declared, the rest inferred) is worse than either: a reader cannot tell which verbs were
|
|
681
|
+
* declared, so an unmarked write still reads as a checked one.
|
|
682
|
+
*
|
|
683
|
+
* What was actually wrong was not the inference, it was the CLAIM. The line asserted
|
|
684
|
+
* `⚠ <action> WRITES` — a fact a name test cannot establish — so a false positive was not merely
|
|
685
|
+
* noise, it was a confident wrong statement about someone's data. Three states, each saying only
|
|
686
|
+
* what it knows:
|
|
687
|
+
*
|
|
688
|
+
* write reads as a write verb → say so, and say it is read from the name
|
|
689
|
+
* read reads as a read verb → no caution
|
|
690
|
+
* unknown neither → say we cannot tell
|
|
691
|
+
*
|
|
692
|
+
* ⚠ The asymmetry that makes a heuristic acceptable is unchanged and now applies to the third
|
|
693
|
+
* state too: over-cautioning on a documentation surface costs a line of text, and an unmarked
|
|
694
|
+
* write costs someone real workspace data.
|
|
695
|
+
*/
|
|
696
|
+
const READ_SEGMENTS = new Set([
|
|
697
|
+
'list', 'get', 'search', 'read', 'find', 'show', 'describe', 'explain', 'status', 'report',
|
|
698
|
+
'stats', 'diff', 'history', 'count', 'counts', 'info', 'catalog', 'perceive', 'look', 'preview',
|
|
699
|
+
'brief', 'log', 'logs', 'map', 'summary', 'view', 'inspect', 'query', 'check', 'ask', 'recall',
|
|
700
|
+
'resolve', 'actions', 'items', 'members', 'columns', 'comments', 'links', 'tree', 'ls', 'cat',
|
|
701
|
+
'grep', 'glob', 'peek', 'fetch', 'estimate', 'validate', 'verify', 'compare', 'suggest',
|
|
702
|
+
'recent', 'feed', 'usage', 'quota', 'whoami', 'ping', 'health', 'schema', 'sql', 'stat',
|
|
703
|
+
]);
|
|
704
|
+
/**
|
|
705
|
+
* ⚠ WRITE WINS A TIE. `document-list` is a read and `note-content-seed` is a write; a name holding
|
|
706
|
+
* both kinds of segment (`board-report-export`) is a write, because that is the direction whose
|
|
707
|
+
* failure costs data.
|
|
708
|
+
*/
|
|
709
|
+
export function classifyExample(action) {
|
|
710
|
+
const segs = String(action ?? '').toLowerCase().split(/[-:._]/);
|
|
711
|
+
if (segs.some((s) => MUTATING_SEGMENTS.has(s)))
|
|
712
|
+
return 'write';
|
|
713
|
+
if (segs.some((s) => READ_SEGMENTS.has(s)))
|
|
714
|
+
return 'read';
|
|
715
|
+
return 'unknown';
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* ⚠ KEPT AS THE BOOLEAN FACE OF `classifyExample`, never a second implementation. Existing guards
|
|
719
|
+
* assert on it in both directions (`note-content-seed` caught, `list-actions` and `board-report`
|
|
720
|
+
* not), and two independent name tests are exactly how the renderer and its guard drift apart.
|
|
721
|
+
*/
|
|
722
|
+
export function exampleMutates(action) {
|
|
723
|
+
return classifyExample(action) === 'write';
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* The Example block, as LINES — pure, so a guard can assert what is printed without a terminal.
|
|
727
|
+
*
|
|
728
|
+
* ⚠⚠ RETURNING LINES RATHER THAN PRINTING IS THE POINT. `render` is `void` and talks to
|
|
729
|
+
* `console.log`, and this file's own history (CBH2-25) is guards that read it as TEXT and called
|
|
730
|
+
* themselves behavioural: "a reviewer broke the fix in one line and they stayed green". A guard
|
|
731
|
+
* still has to prove `render` CALLS this — a correct helper nobody calls proves nothing — so the
|
|
732
|
+
* suite captures `console.log` around the real `render` as well as unit-testing this.
|
|
733
|
+
*/
|
|
734
|
+
export function exampleBlock(action, minimal) {
|
|
735
|
+
const call = ` beehaven call ${action} '${JSON.stringify(minimal)}'`;
|
|
736
|
+
const risk = classifyExample(action);
|
|
737
|
+
/**
|
|
738
|
+
* ⚠⚠ THE HEADING IS THE SAME FOR EVERY RISK CLASS — CBH2-17, AND THE READ BRANCH IS WHY.
|
|
739
|
+
*
|
|
740
|
+
* It used to return `[bold('Example'), call]` for a read: one heading, two behaviours, and the
|
|
741
|
+
* unlabelled half is the one the ticket copy-pasted. `beehaven call document-get
|
|
742
|
+
* '{"id":"example"}'` under a bare heading that says "Example" answers `document not found`.
|
|
743
|
+
*
|
|
744
|
+
* ⚠⚠ THE LABEL IS THE FIX, NOT A BETTER VALUE, AND THAT IS NOT A PREFERENCE — it is forced.
|
|
745
|
+
* An `id` names one row in one workspace, so there is no generic value that could stand in for
|
|
746
|
+
* it; ANY placeholder a synthesizer picks must fail. CBH2-15/16/33 already made these lines
|
|
747
|
+
* VALID against their own schemas (591 of 1,115 print one, all valid) and validity was never the
|
|
748
|
+
* complaint. A line that parses, type-checks and fails is exactly the shape that reads as a
|
|
749
|
+
* working invocation.
|
|
750
|
+
*
|
|
751
|
+
* ⚠ THE *CAUTION* STAYS DIFFERENTIATED, AND THE OLD GUARD'S ARGUMENT FOR THAT IS UNTOUCHED:
|
|
752
|
+
* "a caution on every row is a caution nobody reads". That argument is about the CONSEQUENCE
|
|
753
|
+
* line — a write creates real data, a read merely fails — and none of it applies to a statement
|
|
754
|
+
* about what the values ARE. Reads carry the heading and no third line.
|
|
755
|
+
*/
|
|
756
|
+
const heading = ` ${bold('Example')} ${dim('— SHAPE ONLY. The values are placeholders, not a suggestion.')}`;
|
|
757
|
+
if (risk === 'read')
|
|
758
|
+
return [heading, call];
|
|
759
|
+
const caution = risk === 'write'
|
|
760
|
+
? `⚠ ${action} reads as a WRITE verb (inferred from its name). Run this verbatim and you create real workspace data named "example".`
|
|
761
|
+
: `⚠ ${action}'s name says neither read nor write, so this cannot tell you which it is. Check before running it verbatim — if it writes, you create real workspace data named "example".`;
|
|
762
|
+
return [heading, call, ` ${dim(caution)}`];
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* ⚠⚠ IT ONLY EVER SAW A TYPE NAME, SO IT COULD NOT HAVE BEEN RIGHT (CBH2-33).
|
|
766
|
+
*
|
|
767
|
+
* `synthMinimalInput` called `sampleValue(type)` — a single lowercase string — and threw the rest
|
|
768
|
+
* of the field descriptor away. Everything that makes a value LEGAL rather than merely
|
|
769
|
+
* well-typed lives in the part that was discarded: `enum`, `default`, `const`, `example`. So a
|
|
770
|
+
* constrained field could only ever be filled with a placeholder that the handler must reject:
|
|
771
|
+
*
|
|
772
|
+
* explain canvas-align-blocks → '{"blockIds":[],"axis":"example"}'
|
|
773
|
+
* …while the schema says axis: enum ['left','center-h','right','top','center-v','bottom']
|
|
774
|
+
*
|
|
775
|
+
* 23 required-enum fields across the derived map were emitting a guaranteed-invalid example.
|
|
776
|
+
*
|
|
777
|
+
* ⚠ IT STILL ACCEPTS A BARE TYPE STRING. That is the FieldSchema path and three existing tests
|
|
778
|
+
* pin it; a previous pass on this file regressed exactly that surface. `string | descriptor`
|
|
779
|
+
* keeps the old contract byte-for-byte and adds the new one beside it.
|
|
780
|
+
*
|
|
781
|
+
* ⚠ ORDER: `example` → `default` → `const` → `enum[0]` → type. An author-supplied value outranks
|
|
782
|
+
* anything synthesized, and a declared default is what the handler would have used anyway.
|
|
783
|
+
* `enum[0]` is last of the real values because it is arbitrary among the legal ones — but arbitrary
|
|
784
|
+
* AND legal beats `"example"`, which is neither.
|
|
785
|
+
*/
|
|
786
|
+
function sampleValue(field) {
|
|
787
|
+
if (field !== null && typeof field === 'object') {
|
|
788
|
+
if (field.example !== undefined)
|
|
789
|
+
return field.example;
|
|
790
|
+
if (Array.isArray(field.examples) && field.examples.length > 0)
|
|
791
|
+
return field.examples[0];
|
|
792
|
+
if (field.default !== undefined)
|
|
793
|
+
return field.default;
|
|
794
|
+
if (field.const !== undefined)
|
|
795
|
+
return field.const;
|
|
796
|
+
const e = fieldEnum(field);
|
|
797
|
+
if (e)
|
|
798
|
+
return e[0];
|
|
799
|
+
return sampleValue(describeField(field).type);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* ⚠ THE STRING PATH GOES THROUGH `describeField` TOO, or the zod spelling silently degrades.
|
|
803
|
+
* The live server sends `"ZodNumber?"`, which matches no case below and would fall through to
|
|
804
|
+
* the `'example'` default — a STRING placeholder in a numeric field, which is the first of the
|
|
805
|
+
* three defects CBH2-33 reports. One vocabulary in, one vocabulary out.
|
|
806
|
+
*/
|
|
807
|
+
const type = typeof field === 'string' ? describeField(field).type : 'unknown';
|
|
808
|
+
switch (type.toLowerCase()) {
|
|
809
|
+
case 'string':
|
|
810
|
+
return 'example';
|
|
811
|
+
case 'number':
|
|
812
|
+
case 'real':
|
|
813
|
+
case 'float':
|
|
814
|
+
return 0;
|
|
815
|
+
case 'int':
|
|
816
|
+
case 'integer':
|
|
817
|
+
return 0;
|
|
818
|
+
case 'boolean':
|
|
819
|
+
case 'bool':
|
|
820
|
+
return true;
|
|
821
|
+
case 'array':
|
|
822
|
+
return [];
|
|
823
|
+
case 'object':
|
|
824
|
+
case 'json':
|
|
825
|
+
return {};
|
|
826
|
+
case 'date':
|
|
827
|
+
return new Date().toISOString();
|
|
828
|
+
default:
|
|
829
|
+
/**
|
|
830
|
+
* ⚠ NEVER `null`. An unrecognised type used to fall through to null, which renders as
|
|
831
|
+
* `{"title":null}` — a value that reads as "explicitly blank" rather than "we did not know",
|
|
832
|
+
* and which several handlers accept and store. A placeholder string is obviously a
|
|
833
|
+
* placeholder; `null` is obviously nothing, and the two are not confusable in a paste.
|
|
834
|
+
*/
|
|
835
|
+
return 'example';
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// ── format helpers ─────────────────────────────────────────────────────
|
|
839
|
+
function bold(s) {
|
|
840
|
+
return `\x1b[1m${s}\x1b[0m`;
|
|
841
|
+
}
|
|
842
|
+
function dim(s) {
|
|
843
|
+
return `\x1b[2m${s}\x1b[0m`;
|
|
844
|
+
}
|
|
845
|
+
function indent(s) {
|
|
846
|
+
return s
|
|
847
|
+
.split('\n')
|
|
848
|
+
.map((l) => ' ' + l.trim())
|
|
849
|
+
.join('\n');
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Exposed for tests.
|
|
853
|
+
*
|
|
854
|
+
* ⚠⚠ `schemaRows` / `formatSchemaRows` ARE HERE SO THE GUARDS CAN RUN THE CODE (CBH2-25).
|
|
855
|
+
* A previous pass on this file shipped guards that read `explain.ts` as TEXT and called themselves
|
|
856
|
+
* behavioural in their own docstrings; a reviewer broke the fix in one line and they stayed green.
|
|
857
|
+
* Anything a guard needs to assert about rendering must be reachable as a function, not as a
|
|
858
|
+
* regex over the source — so the extraction IS the fix, not tidying.
|
|
859
|
+
*/
|
|
860
|
+
export const __testing = {
|
|
861
|
+
normalize,
|
|
862
|
+
describeField,
|
|
863
|
+
synthMinimalInput,
|
|
864
|
+
sampleValue,
|
|
865
|
+
schemaRows,
|
|
866
|
+
formatSchemaRows,
|
|
867
|
+
mergeInputSchemas,
|
|
868
|
+
normalizeTypeName,
|
|
869
|
+
exampleMutates,
|
|
870
|
+
classifyExample,
|
|
871
|
+
aliasGroups,
|
|
872
|
+
exampleBlock,
|
|
873
|
+
/**
|
|
874
|
+
* ⚠⚠ `render` IS EXPORTED SO A GUARD CAN ASSERT THE REAL CALL SITE, NOT JUST THE HELPER (CBH2-57).
|
|
875
|
+
*
|
|
876
|
+
* `exampleBlock` being correct proves nothing about what `explain` prints — the previous version
|
|
877
|
+
* of this defect was exactly that shape, a generator that had been fixed while the line a user
|
|
878
|
+
* reads had not. The suite captures `console.log` around this function and asserts the actual
|
|
879
|
+
* output, so deleting the call and inlining the old one-liner fails.
|
|
880
|
+
*/
|
|
881
|
+
render,
|
|
882
|
+
};
|