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,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ephemeral sandboxed MCP (#81) — per-session, time-boxed MCP server
|
|
3
|
+
* instances that auto-terminate when the agent turn ends OR the
|
|
4
|
+
* timeout fires.
|
|
5
|
+
*
|
|
6
|
+
* Why: many MCP servers are "useful for one task" — a Gmail server
|
|
7
|
+
* for a triage session, a Stripe sandbox key for a billing prototype.
|
|
8
|
+
* Keeping them long-lived (the #80 + #82 pattern) is wrong for these
|
|
9
|
+
* cases. This module spawns them on-demand with:
|
|
10
|
+
* - per-session env injection (auth tokens, sandbox keys)
|
|
11
|
+
* - 10-minute idle timeout (configurable)
|
|
12
|
+
* - SIGTERM on session end OR daemon shutdown
|
|
13
|
+
* - process-table tracking so `daemon-mcp-sandbox-list` reports
|
|
14
|
+
* which sandboxes are alive
|
|
15
|
+
*
|
|
16
|
+
* Usage from the daemon's IPC layer:
|
|
17
|
+
*
|
|
18
|
+
* beehaven mcp-sandbox start \
|
|
19
|
+
* --command "uvx mcp-server-stripe-sandbox" \
|
|
20
|
+
* --env STRIPE_KEY=sk_test_... \
|
|
21
|
+
* --session-id <fab-session>
|
|
22
|
+
*
|
|
23
|
+
* → returns { sandboxId, mcpEndpoint: 'stdio:...' }
|
|
24
|
+
*
|
|
25
|
+
* Agent uses the sandboxId in its tool definitions; daemon
|
|
26
|
+
* terminates on session end.
|
|
27
|
+
*/
|
|
28
|
+
import { spawn } from 'child_process';
|
|
29
|
+
const _sandboxes = new Map();
|
|
30
|
+
const SWEEP_INTERVAL_MS = 60_000;
|
|
31
|
+
let _sweepTimer = null;
|
|
32
|
+
/**
|
|
33
|
+
* Spawn an ephemeral MCP server bound to a session.
|
|
34
|
+
*
|
|
35
|
+
* Returns the sandbox descriptor. Caller pipes its stdin/stdout for
|
|
36
|
+
* MCP protocol traffic (or the daemon's MCP-bridge does this on
|
|
37
|
+
* their behalf).
|
|
38
|
+
*/
|
|
39
|
+
export function startMcpSandbox(args) {
|
|
40
|
+
const id = `mcps-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
41
|
+
const proc = spawn(args.command, args.commandArgs ?? [], {
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
env: { ...process.env, ...(args.env ?? {}) },
|
|
44
|
+
});
|
|
45
|
+
const sandbox = {
|
|
46
|
+
id,
|
|
47
|
+
sessionId: args.sessionId,
|
|
48
|
+
command: args.command,
|
|
49
|
+
args: args.commandArgs ?? [],
|
|
50
|
+
env: args.env ?? {},
|
|
51
|
+
proc: proc,
|
|
52
|
+
startedAt: Date.now(),
|
|
53
|
+
lastActive: Date.now(),
|
|
54
|
+
idleTimeoutMs: args.idleTimeoutMs ?? 10 * 60 * 1000,
|
|
55
|
+
};
|
|
56
|
+
_sandboxes.set(id, sandbox);
|
|
57
|
+
proc.on('exit', () => {
|
|
58
|
+
_sandboxes.delete(id);
|
|
59
|
+
});
|
|
60
|
+
// Lazy-start the sweep timer the first time any sandbox spawns.
|
|
61
|
+
if (!_sweepTimer) {
|
|
62
|
+
_sweepTimer = setInterval(sweepIdleSandboxes, SWEEP_INTERVAL_MS);
|
|
63
|
+
// Don't keep the daemon alive solely for the sweep timer.
|
|
64
|
+
if (typeof _sweepTimer?.unref === 'function') {
|
|
65
|
+
_sweepTimer.unref();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return sandbox;
|
|
69
|
+
}
|
|
70
|
+
/** Mark a sandbox as recently-used (called on every MCP protocol
|
|
71
|
+
* message routed through it). */
|
|
72
|
+
export function touchMcpSandbox(id) {
|
|
73
|
+
const sandbox = _sandboxes.get(id);
|
|
74
|
+
if (sandbox)
|
|
75
|
+
sandbox.lastActive = Date.now();
|
|
76
|
+
}
|
|
77
|
+
/** Terminate a specific sandbox. */
|
|
78
|
+
export function stopMcpSandbox(id) {
|
|
79
|
+
const sandbox = _sandboxes.get(id);
|
|
80
|
+
if (!sandbox)
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
sandbox.proc.kill('SIGTERM');
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
_sandboxes.delete(id);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Terminate every sandbox bound to a session — called when the
|
|
90
|
+
* agent turn ends OR on daemon shutdown. */
|
|
91
|
+
export function stopMcpSandboxesForSession(sessionId) {
|
|
92
|
+
let stopped = 0;
|
|
93
|
+
for (const [id, sandbox] of _sandboxes) {
|
|
94
|
+
if (sandbox.sessionId === sessionId) {
|
|
95
|
+
try {
|
|
96
|
+
sandbox.proc.kill('SIGTERM');
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
_sandboxes.delete(id);
|
|
100
|
+
stopped++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return stopped;
|
|
104
|
+
}
|
|
105
|
+
/** List active sandboxes for daemon-status JSON. */
|
|
106
|
+
export function listMcpSandboxes() {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
return [..._sandboxes.values()].map((s) => ({
|
|
109
|
+
id: s.id,
|
|
110
|
+
sessionId: s.sessionId,
|
|
111
|
+
command: s.command,
|
|
112
|
+
ageMs: now - s.startedAt,
|
|
113
|
+
idleMs: now - s.lastActive,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
function sweepIdleSandboxes() {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
for (const [id, sandbox] of _sandboxes) {
|
|
119
|
+
if (now - sandbox.lastActive > sandbox.idleTimeoutMs) {
|
|
120
|
+
try {
|
|
121
|
+
sandbox.proc.kill('SIGTERM');
|
|
122
|
+
}
|
|
123
|
+
catch { }
|
|
124
|
+
_sandboxes.delete(id);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* daemon-omni-company — run the workspace like a company, not like a chat.
|
|
6
|
+
*
|
|
7
|
+
* ## What this is
|
|
8
|
+
*
|
|
9
|
+
* A prompt ADDON, selected by a flag, that puts Omni into a mode where the deliverable is the
|
|
10
|
+
* WORKSPACE rather than the reply: a request produces a PRD, a design doc, tickets, designs and a
|
|
11
|
+
* QA pass, each as a real artifact somebody else could pick up — the way a large engineering
|
|
12
|
+
* organisation produces work.
|
|
13
|
+
*
|
|
14
|
+
* ⚠⚠ IT IS AN ADDON, NOT A REPLACEMENT. `OMNI_IDENTITY` still leads. Every safety property lives
|
|
15
|
+
* there — what may be run, what may never be done unasked, the git rules, when to stop and ask —
|
|
16
|
+
* and a mode that replaced it would drop all of them to gain a role-play. This says HOW the work is
|
|
17
|
+
* recorded; the identity says who is doing it and what they may not do.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ IT SITS BEFORE THE CACHE BOUNDARY. The text is static per build, so it belongs in the cacheable
|
|
20
|
+
* prefix beside the identity. Appending it per turn as `appendSystemPrompt` would push ~6 KB across
|
|
21
|
+
* the boundary on every single turn — the prefix-cache mistake `resolveSystemPrompt` was split to
|
|
22
|
+
* avoid.
|
|
23
|
+
*
|
|
24
|
+
* ## ⚠⚠ What this mode DELIBERATELY DOES NOT DO
|
|
25
|
+
*
|
|
26
|
+
* **It does not role-play a cast of named people.** `THOUGHT_AGENT` notes record why: role-played
|
|
27
|
+
* org charts — "you are the CEO, now switch to the VP" — were refused there as "a fabrication
|
|
28
|
+
* engine", and that judgement holds. A team here is a **discipline with a deliverable and a gate**,
|
|
29
|
+
* not a character with a name and a personality. The value is the artifact trail, and a model
|
|
30
|
+
* inventing a design director's opinion produces neither better artifacts nor honest ones.
|
|
31
|
+
*
|
|
32
|
+
* **It does not invent approvals.** No stage may mark itself reviewed by somebody who does not
|
|
33
|
+
* exist. A gate is passed by producing the artifact, never by narrating that a teammate signed off.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* The workspace this mode is meant to run in.
|
|
37
|
+
*
|
|
38
|
+
* ⚠ NAMED, NOT ASSUMED. A mode that fills a workspace with PRDs and tickets must not do it to
|
|
39
|
+
* whichever workspace the daemon happened to be pointed at — `beehaven connect` is machine-global
|
|
40
|
+
* and another session can move it. The prompt says the name out loud so the agent checks before it
|
|
41
|
+
* writes, and `beehaven pwd` is the check.
|
|
42
|
+
*/
|
|
43
|
+
export const COMPANY_WORKSPACE = 'hive2';
|
|
44
|
+
/**
|
|
45
|
+
* The same workspace by id, because the DAEMON only knows the id.
|
|
46
|
+
*
|
|
47
|
+
* ⚠⚠ WITHOUT THIS THE DIAGNOSTIC LIES. `connectedBuilding` carries a `buildingId`, and comparing
|
|
48
|
+
* an id against the string `'hive2'` would report "connected to the wrong workspace" on a daemon
|
|
49
|
+
* that is connected to exactly the right one — the false alarm this diagnostic's own `null` case
|
|
50
|
+
* was written to avoid, arriving through the comparison instead.
|
|
51
|
+
*
|
|
52
|
+
* ⚠ OVERRIDABLE, because an id is deployment-specific in a way a name is not. Empty means "ids
|
|
53
|
+
* cannot be matched here", which resolves to `null` — unknown — rather than to a false mismatch.
|
|
54
|
+
*/
|
|
55
|
+
export const COMPANY_WORKSPACE_ID = String(process.env.CLEARLY_COMPANY_WORKSPACE_ID ?? '').trim();
|
|
56
|
+
/** `CLEARLY_COMPANY_MODE=1` turns it on. Off by default — this is a dev-only surface today. */
|
|
57
|
+
export function isCompanyMode(env = process.env) {
|
|
58
|
+
const v = String(env.CLEARLY_COMPANY_MODE ?? '').trim().toLowerCase();
|
|
59
|
+
return v === '1' || v === 'true' || v === 'on';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Where the persisted switch lives.
|
|
63
|
+
*
|
|
64
|
+
* ⚠⚠ AN ENV VAR IS THE WRONG ONLY-SWITCH FOR A DAEMON, AND THIS IS THE FAILURE IT CAUSED. The
|
|
65
|
+
* daemon is spawned by the Mac app (or by `beehaven start --headless`), so `CLEARLY_COMPANY_MODE=1`
|
|
66
|
+
* typed in a terminal reaches the SHELL THAT TALKS TO the daemon and never the daemon itself.
|
|
67
|
+
* Measured on this machine: the feature was written, wired, tested — and `ps eww` on the running
|
|
68
|
+
* daemon showed the variable absent, so company mode had been OFF the whole time. `companyDiagnostics`
|
|
69
|
+
* exists to catch exactly that and warns about it in those words; nobody could run it (see below).
|
|
70
|
+
*
|
|
71
|
+
* ⚠ ITS OWN FILE, NOT `config.json`. That one holds relay credentials and tokens — a mode switch has
|
|
72
|
+
* no business in a secrets file, and this repo's own notes record the permission classifier blocking
|
|
73
|
+
* hand-edits to it. Same shape as the `target-*.json` files beside it.
|
|
74
|
+
*/
|
|
75
|
+
export const COMPANY_MODE_FILE = join(homedir(), '.beehaven', 'company-mode.json');
|
|
76
|
+
/** The persisted switch. Unreadable / absent / malformed ⇒ OFF, never a throw. */
|
|
77
|
+
export function readCompanyMode(file = COMPANY_MODE_FILE) {
|
|
78
|
+
try {
|
|
79
|
+
const raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
80
|
+
return {
|
|
81
|
+
enabled: raw?.enabled === true,
|
|
82
|
+
...(raw?.workspaceId ? { workspaceId: String(raw.workspaceId) } : {}),
|
|
83
|
+
...(raw?.updatedAt ? { updatedAt: Number(raw.updatedAt) } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { enabled: false };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Persist the switch. Returns what was written so a caller can report it back verbatim. */
|
|
91
|
+
export function writeCompanyMode(patch, file = COMPANY_MODE_FILE) {
|
|
92
|
+
const next = { ...readCompanyMode(file), ...patch, updatedAt: Date.now() };
|
|
93
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
94
|
+
writeFileSync(file, JSON.stringify(next, null, 2));
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* IS THE MODE ON? — the question the daemon actually asks.
|
|
99
|
+
*
|
|
100
|
+
* ⚠⚠ TWO NAMES BECAUSE THERE ARE TWO QUESTIONS, and collapsing them would lose the one the
|
|
101
|
+
* diagnostic needs. `isCompanyMode` answers *"did the operator set the env var"*; this answers
|
|
102
|
+
* *"is the mode on"*. The gap between those two is precisely the failure above — the var set on the
|
|
103
|
+
* wrong process — so a diagnostic that cannot tell them apart cannot report it.
|
|
104
|
+
*
|
|
105
|
+
* ⚠ THE ENV VAR WINS WHEN IT IS SET TO ANYTHING AT ALL, including `0`. An explicit
|
|
106
|
+
* `CLEARLY_COMPANY_MODE=0` must be able to turn the mode OFF on a machine whose file says on —
|
|
107
|
+
* otherwise the env var is not an override, it is a one-way latch.
|
|
108
|
+
*/
|
|
109
|
+
export function companyModeEnabled(env = process.env, file = COMPANY_MODE_FILE) {
|
|
110
|
+
if (String(env.CLEARLY_COMPANY_MODE ?? '').trim())
|
|
111
|
+
return isCompanyMode(env);
|
|
112
|
+
return readCompanyMode(file).enabled;
|
|
113
|
+
}
|
|
114
|
+
/** Where the answer came from — reported, because "set on the wrong process" has no other symptom. */
|
|
115
|
+
export function companyModeSource(env = process.env, file = COMPANY_MODE_FILE) {
|
|
116
|
+
if (String(env.CLEARLY_COMPANY_MODE ?? '').trim())
|
|
117
|
+
return isCompanyMode(env) ? 'env' : 'off';
|
|
118
|
+
return readCompanyMode(file).enabled ? 'persisted' : 'off';
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Everything an operator needs to answer "is company mode actually working", in one object.
|
|
122
|
+
*
|
|
123
|
+
* ⚠⚠ IT REPORTS STATE, NOT EVENTS. A log says what happened during turns somebody ran; it cannot
|
|
124
|
+
* say that the mode has been silently OFF all week because the env var never reached the daemon, or
|
|
125
|
+
* that the daemon drifted onto the wrong workspace after somebody ran `beehaven connect` in a
|
|
126
|
+
* terminal. Those are the two failures that produce no error and no output — the same reasoning
|
|
127
|
+
* `thought-stats` exists for.
|
|
128
|
+
*
|
|
129
|
+
* ⚠ PURE, AND IT TAKES ITS INPUTS. It cannot read the daemon's connection or the identity registry
|
|
130
|
+
* itself without importing half the daemon, and a diagnostic that drags in what it inspects is a
|
|
131
|
+
* diagnostic that stops working exactly when the thing it inspects is broken.
|
|
132
|
+
*/
|
|
133
|
+
export function companyDiagnostics(input = {}) {
|
|
134
|
+
const env = input.env ?? process.env;
|
|
135
|
+
const modeFile = input.modeFile ?? COMPANY_MODE_FILE;
|
|
136
|
+
const on = companyModeEnabled(env, modeFile);
|
|
137
|
+
const source = companyModeSource(env, modeFile);
|
|
138
|
+
const identity = String(input.identity ?? '');
|
|
139
|
+
const connected = input.connectedWorkspace ?? null;
|
|
140
|
+
/**
|
|
141
|
+
* ⚠ THE MATCH IS BY NAME AND IS `null` WHEN UNKNOWN, never `false`. `beehaven workspaces` prints
|
|
142
|
+
* ids; a caller that could not resolve the name must not be reported as "connected to the wrong
|
|
143
|
+
* workspace" — that reads as a fault and sends someone looking for one.
|
|
144
|
+
*/
|
|
145
|
+
/**
|
|
146
|
+
* ⚠ THE ID MAY BE PERSISTED TOO. `beehaven omni company on --workspace <id>` records it, so an
|
|
147
|
+
* operator can make the match checkable without setting a build-time env var — which is the same
|
|
148
|
+
* reason the switch itself is persisted.
|
|
149
|
+
*/
|
|
150
|
+
const expectedId = COMPANY_WORKSPACE_ID || String(readCompanyMode(modeFile).workspaceId ?? '');
|
|
151
|
+
const matches = connected == null
|
|
152
|
+
? null
|
|
153
|
+
: (connected === COMPANY_WORKSPACE || (!!expectedId && connected === expectedId)
|
|
154
|
+
? true
|
|
155
|
+
// ⚠ An id we cannot check is UNKNOWN, not wrong. Without `COMPANY_WORKSPACE_ID` configured
|
|
156
|
+
// every id-shaped label would read as a mismatch and send someone hunting a fault.
|
|
157
|
+
: (!expectedId && /^[0-9a-f]{32,}$/i.test(connected) ? null : false));
|
|
158
|
+
const identityBytes = identity.length;
|
|
159
|
+
const addonBytes = on ? COMPANY_ADDON.length : 0;
|
|
160
|
+
const totalBytes = identityBytes + addonBytes;
|
|
161
|
+
const warnings = [];
|
|
162
|
+
if (!on)
|
|
163
|
+
warnings.push('company mode is OFF — turn it on with `beehaven omni company on` (persisted, survives a daemon restart). CLEARLY_COMPANY_MODE=1 also works, but it must be set on the DAEMON\u2019s environment, not on the shell that talks to it — which is how this was off for the whole time it appeared to be built.');
|
|
164
|
+
if (on && !identity)
|
|
165
|
+
warnings.push('no Omni identity registered — the addon is running on the bare claude_code preset, without the safety rules the identity carries');
|
|
166
|
+
if (on && matches === false)
|
|
167
|
+
warnings.push(`daemon is connected to "${connected}", not "${COMPANY_WORKSPACE}" — this mode writes artifacts and must not fill the wrong workspace`);
|
|
168
|
+
if (on && matches === null)
|
|
169
|
+
warnings.push('could not resolve the connected workspace — run `beehaven pwd` before trusting any write');
|
|
170
|
+
if (on && input.claudeBinary === null)
|
|
171
|
+
warnings.push('no claude binary resolved — turns will refuse until Claude Code is installed');
|
|
172
|
+
return {
|
|
173
|
+
mode: on ? 'on' : 'off',
|
|
174
|
+
source,
|
|
175
|
+
workspace: { expected: COMPANY_WORKSPACE, connected, matches },
|
|
176
|
+
identityRegistered: !!identity,
|
|
177
|
+
// ⚠ 4 bytes/token is a rough divisor, and it is here so the number is COMPARABLE across builds
|
|
178
|
+
// rather than precise. The point is noticing the prompt doubling, not billing for it.
|
|
179
|
+
prompt: { identityBytes, addonBytes, totalBytes, approxTokens: Math.round(totalBytes / 4) },
|
|
180
|
+
requiredVerbs: COMPANY_REQUIRED_VERBS,
|
|
181
|
+
claudeBinary: input.claudeBinary ?? null,
|
|
182
|
+
warnings,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The verbs the addon actually names.
|
|
187
|
+
*
|
|
188
|
+
* ⚠⚠ KEEP THIS IN STEP WITH THE PROMPT. It exists so a missing GRANT is visible as a diagnostic
|
|
189
|
+
* rather than as an agent quietly failing mid-sprint — `dev-report` was ungranted when this mode
|
|
190
|
+
* was written, which would have made the "file the tooling gap" instruction refuse every time
|
|
191
|
+
* while reading perfectly in the prompt. A guard asserts every name here appears in the addon.
|
|
192
|
+
*/
|
|
193
|
+
export const COMPANY_REQUIRED_VERBS = [
|
|
194
|
+
'document-create', 'document-edit', 'document-append', 'document-link-resolve',
|
|
195
|
+
'board-create', 'ticket-create', 'ticket-update',
|
|
196
|
+
'composition-create', 'canvas-act',
|
|
197
|
+
'sheet-create', 'sheet-set', 'sheet-append',
|
|
198
|
+
'project-create', 'project-list',
|
|
199
|
+
// ⚠ THE DELIVERY VERBS. Project management is a discipline here, not a synonym for the board,
|
|
200
|
+
// and these are what it actually uses — a sprint nobody can plan or measure is a label.
|
|
201
|
+
'sprint-create', 'sprint-plan', 'sprint-start', 'sprint-complete', 'sprint-list',
|
|
202
|
+
'ticket-link-create', 'ticket-links', 'burndown',
|
|
203
|
+
// ⚠ THE LIFECYCLE EDGE. `work-status` is how a stage is READ; `work-unlink` is the only way to
|
|
204
|
+
// clear a dead artifact link, and without it `brokenLinks` is a number that only goes up.
|
|
205
|
+
'work-status', 'work-links', 'work-link', 'work-unlink',
|
|
206
|
+
'dev-report',
|
|
207
|
+
'_rpc:catalog', 'describe-action',
|
|
208
|
+
];
|
|
209
|
+
/**
|
|
210
|
+
* The verbs each stage actually reaches for. Injected per turn, so the model is told about the
|
|
211
|
+
* FIVE it needs rather than the forty it might.
|
|
212
|
+
*
|
|
213
|
+
* ⚠ NOT A TOOL GRANT. Everything remains reachable; this is a pointer, not a palette. Narrowing
|
|
214
|
+
* what is *available* per stage would break the ordinary case where a turn spans two of them.
|
|
215
|
+
*/
|
|
216
|
+
const STAGE_VERBS = {
|
|
217
|
+
prd: 'document-create { ticketId, artifactKind:"prd" } · document-edit · ticket-update',
|
|
218
|
+
design: 'composition-create · canvas-act (canvas.compose) · work-link { kind:"design", refKind:"composition" }',
|
|
219
|
+
'eng-design': 'document-create { ticketId, artifactKind:"eng-design" } · bash · read · edit',
|
|
220
|
+
qa: 'bash · document-create { ticketId, artifactKind:"qa" } · ticket-update',
|
|
221
|
+
done: 'ticket-update · work-link { kind:"pr", refKind:"url" }',
|
|
222
|
+
};
|
|
223
|
+
/** What a stage is FOR, in one clause — the judgement the verbs cannot carry. */
|
|
224
|
+
const STAGE_INTENT = {
|
|
225
|
+
prd: 'state the problem, who has it, what done looks like, what is out of scope, and how you would know it worked',
|
|
226
|
+
design: 'show the screens and the states that actually fail — empty, loading, error — not only the happy path',
|
|
227
|
+
'eng-design': 'state the approach, what was rejected and why, the data model, and what breaks at scale',
|
|
228
|
+
qa: 'write what was tested, what passed, what did not, and what is knowingly untested',
|
|
229
|
+
done: 'every gate is met — close it, and link the PR if there is one',
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* The pre-turn block: where this work stands, and what this stage is for.
|
|
233
|
+
*
|
|
234
|
+
* ⚠⚠ EVERY WORD OF IT IS DERIVED FROM THE BOARD. That is the whole design. A block that ASSERTED a
|
|
235
|
+
* stage could contradict the card the moment somebody moved it in the UI — and a model handed two
|
|
236
|
+
* contradictory inventories does not average them, it follows whichever it read last. Derived, it
|
|
237
|
+
* cannot disagree with the thing it describes.
|
|
238
|
+
*
|
|
239
|
+
* ⚠ IT REPLACES FOUR FIFTHS OF THE STANDING ADDON. That text describes five disciplines, four of
|
|
240
|
+
* which are irrelevant on any given turn, and it is paid on every one. This is ~120 tokens and it
|
|
241
|
+
* is about the turn actually happening.
|
|
242
|
+
*
|
|
243
|
+
* ⚠ NO ACTIVE CARD ⇒ NO BLOCK. Silence is correct: a turn with no work in front of it is a
|
|
244
|
+
* conversation, and inventing a stage for it is how every question starts minting PRDs.
|
|
245
|
+
*/
|
|
246
|
+
export function workContextBlock(work) {
|
|
247
|
+
if (!work)
|
|
248
|
+
return '';
|
|
249
|
+
const met = Object.entries(work.gates).filter(([, v]) => v).map(([k]) => k);
|
|
250
|
+
const stage = work.stage || 'prd';
|
|
251
|
+
const lines = [
|
|
252
|
+
`## This turn's work`,
|
|
253
|
+
`**${work.key ?? '(unkeyed)'} — ${work.title}** · ${work.status}`,
|
|
254
|
+
met.length ? `Done: ${met.join(', ')}` : 'Nothing recorded against it yet.',
|
|
255
|
+
work.nextGate
|
|
256
|
+
? `**Next gate: ${work.nextGate}** — ${STAGE_INTENT[stage] ?? ''}`
|
|
257
|
+
: `All gates met — ${STAGE_INTENT.done}`,
|
|
258
|
+
`Reach for: ${STAGE_VERBS[stage] ?? STAGE_VERBS.prd}`,
|
|
259
|
+
];
|
|
260
|
+
/**
|
|
261
|
+
* ⚠ A BROKEN LINK IS SURFACED, not silently excluded. `work-status` already refuses to count it
|
|
262
|
+
* toward a gate; if nobody is ever TOLD, the card quietly needs a fix nobody makes.
|
|
263
|
+
*/
|
|
264
|
+
if (work.brokenLinks > 0) {
|
|
265
|
+
lines.push(`⚠ ${work.brokenLinks} link(s) on this card point at something deleted — \`work-links\` shows which.`);
|
|
266
|
+
}
|
|
267
|
+
if (work.pr.length)
|
|
268
|
+
lines.push(`PR: ${work.pr.join(' · ')}`);
|
|
269
|
+
/**
|
|
270
|
+
* ⚠⚠ THE CARVE-OUT SURVIVES THE INJECTION. Without this line the block reads as an instruction to
|
|
271
|
+
* advance the gate whatever was asked, and every "what do you think?" turns into a PRD — the
|
|
272
|
+
* failure the no-unasked-cards restraint exists for. The stage says where the work stands; it
|
|
273
|
+
* does not decide that this turn is work.
|
|
274
|
+
*/
|
|
275
|
+
lines.push('⚠ This says where the work stands. If they asked a question, answer it — a question is not a stage.');
|
|
276
|
+
return lines.join('\n');
|
|
277
|
+
}
|
|
278
|
+
export const COMPANY_ADDON = [
|
|
279
|
+
`# Operating mode: the workspace is the company`,
|
|
280
|
+
`You are running as an engineering organisation rather than as an assistant in a chat. The person you are talking to is a stakeholder, not a ticket queue: they describe what they want in ordinary language, and **you produce the artifacts a company would produce.** They should never have to ask you to write a PRD, open a ticket, make a design or file a doc. If the work needs one, it exists by the time you reply.`,
|
|
281
|
+
`**The workspace \`${COMPANY_WORKSPACE}\` is the source of truth.** Not this conversation. A decision that exists only in a message you sent has not been made — it is lost the moment the thread scrolls. Before you write anything, confirm where you are with \`beehaven pwd\`; if it is not \`${COMPANY_WORKSPACE}\`, connect to it and say so in one clause. Never fill a workspace you were not asked to fill.`,
|
|
282
|
+
`## The disciplines`,
|
|
283
|
+
/**
|
|
284
|
+
* ⚠⚠ NAMED, NOT DESCRIBED — the per-turn block carries the ACTIVE one in full.
|
|
285
|
+
*
|
|
286
|
+
* This used to be five paragraphs explaining what a PRD is, what a design owes, what QA's
|
|
287
|
+
* deliverable is. Four fifths of that is irrelevant on any given turn and it was paid on every
|
|
288
|
+
* one. `workContextBlock` injects the stage the work is actually at, its intent, and the verbs
|
|
289
|
+
* it uses — ~120 tokens, derived from the board, about the turn that is happening.
|
|
290
|
+
*/
|
|
291
|
+
`Five disciplines, each with a deliverable: **product** owns the PRD · **design** owns the screens and their failure states · **engineering** owns the design document and the code · **QA** owns a written verification · **project management** owns the plan — the sprint, the sequence, the dependencies and the honest forecast. They are DISCIPLINES, not people: do not name colleagues, do not invent their opinions, and never record that someone reviewed something. A gate is passed by producing the artifact.`,
|
|
292
|
+
/**
|
|
293
|
+
* ⚠⚠ PROJECT MANAGEMENT IS NOT A GATE, AND MUST NOT BECOME ONE. The four gates are per-CARD and
|
|
294
|
+
* ask *is this piece of work justified*. Delivery asks a question one level up — *is the set of
|
|
295
|
+
* work going to land* — so adding a `pm` gate would demand a planning artifact on every single
|
|
296
|
+
* card, which is how a tracker becomes paperwork. `GATE_ORDER` stays four.
|
|
297
|
+
*/
|
|
298
|
+
`⚠ **Project management works on the SPRINT, not on the card.** Its questions are what we committed to, what blocks what, and whether the forecast is real — so its deliverables are the sprint, the dependency links and the plan, never a per-card artifact. \`ticket-link-create { fromId, toId, linkType:"blocks" }\` records a dependency (both ends take a key); \`ticket-links\` reads them back and says whether a card is blocked; \`burndown { kind:"velocity" }\` is what a forecast is allowed to be based on. **Size cards with \`points\` — a TOP-LEVEL field, not a member of \`data\`, which counts as zero and makes every figure under-report.**`,
|
|
299
|
+
`## The gates`,
|
|
300
|
+
/**
|
|
301
|
+
* ⚠⚠ THE GATES ARE NOT LISTED HERE ANY MORE — THEY ARE READ. `work-status` answers which are met
|
|
302
|
+
* for the active card, and the pre-turn block names the next unmet one. A gate written as prose
|
|
303
|
+
* is one the model can talk itself out of; a gate read from `ticket_artifacts` is a fact, and one
|
|
304
|
+
* pointing at a DELETED artifact correctly reports as unmet.
|
|
305
|
+
*/
|
|
306
|
+
`Work moves through PRD → design → engineering design → QA, and the board knows where it is. Call \`work-status { ticketId }\` when you need to know; \`document-create { ticketId, artifactKind }\` records an artifact against a card and advances it in the same call, so you never have to move a card by hand.`,
|
|
307
|
+
`⚠ **The gates apply to WORK, not to conversation.** A question, an opinion, an explanation, a bit of analysis — those are answered, not documented. Filing a PRD because someone asked "what do you think about X?" is exactly the failure that makes a workspace unusable: it fills with artifacts nobody chose and the real ones become impossible to find. **When you are thinking WITH them, just think with them.**`,
|
|
308
|
+
`## Ceremonies`,
|
|
309
|
+
`**Sprint planning.** When a body of work starts, or when the current sprint's cards are done: \`sprint-create\` it, \`sprint-plan\` the cards into it with sizes, \`sprint-start\` it, and write the plan as a document — what is in, what is deliberately not, and what the sprint is trying to learn. ⚠ **Base what fits on \`burndown { kind:"velocity" }\`, not on optimism**: a plan the last three sprints say is impossible is a wish. ⚠ Check \`sprint-list { state:"active" }\` first — only one sprint per board may be active, and starting a second is refused. Report it in two or three sentences, not as a document dump.`,
|
|
310
|
+
`**Design sprint.** For anything genuinely new, before committing to a build: produce two or three DIFFERENT approaches as separate compositions, write down the tradeoff for each, and recommend one with the reason. ⚠ Two options where one is obviously bad is not a design sprint — it is a rubber stamp with extra steps. If you cannot make a real case for the second option, say so instead of inventing one.`,
|
|
311
|
+
`**Retro.** After a body of work lands: \`sprint-complete\` it (unfinished cards go back to the backlog unless you say otherwise), then one document — what worked, what did not, what changes next time, and the actual numbers from \`burndown\` rather than an impression of them. Short. A retro nobody reads is worse than none.`,
|
|
312
|
+
`## Everything goes through the beehaven CLI`,
|
|
313
|
+
`The workspace is reached with \`beehaven call <verb>\`. Discover verbs with \`beehaven call _rpc:catalog\` and read one with \`beehaven call describe-action '{"name":"..."}'\` — every granted action carries a description, so you never have to guess an argument shape.`,
|
|
314
|
+
`The artifacts you will use most:`,
|
|
315
|
+
`- **PRDs, design docs, plans, retros** → \`document-create\` / \`document-edit\` / \`document-append\`. Not \`document-update\` unless you mean to replace the whole body.`,
|
|
316
|
+
`- **Tickets and sprints** → \`board-create\`, \`ticket-create\` (needs a \`boardId\`), \`ticket-update\`, \`sprint-create\` / \`sprint-plan\` / \`sprint-start\` / \`sprint-complete\` / \`sprint-list\`, and \`ticket-link-create\` for dependencies.`,
|
|
317
|
+
`- **Where the work stands** → \`work-status { ticketId }\` reads the gates; \`work-links\` lists what is attached and what is DEAD; \`work-link\` attaches something you did not make with \`document-create\` (a design composition, a PR url); \`work-unlink\` removes a dead edge. ⚠ All of them take a **key** (\`CLR-42\`), not only a uuid.`,
|
|
318
|
+
`- **Designs** → \`composition-create\` then \`canvas-act\`. Reach for \`canvas.compose\` before hand-placing rectangles — it lands a whole spec-correct block with its real text in one call.`,
|
|
319
|
+
`- **Spreadsheets** → \`sheet-create\` / \`sheet-set\` / \`sheet-append\` for anything tabular: estimates, test matrices, comparisons.`,
|
|
320
|
+
`- **Filing** → every artifact belongs to a project. ⚠ **\`project-list\` FIRST** — a project you cannot see is one you will create again, and this workspace already carries two projects with the same name from exactly that. \`project-create\` only when nothing fits, then \`projectId\` on everything after.`,
|
|
321
|
+
`⚠⚠ **THE CLI ACCEPTS ARGUMENTS IT IGNORES AND ANSWERS \`ok: true\` HAVING STORED NOTHING.** A field the schema does not have is dropped silently — a document created with \`content\` instead of \`markdown\` exists, opens, and is invisible to search forever. **After any write, read it back.** Do not report a PRD written until you have seen it come back. A write you did not verify is a write you are guessing about, and the stakeholder will believe you.`,
|
|
322
|
+
`⚠ **The exception stands: tickets and boards go through your TYPED tools where you have them**, not \`beehaven call ticket-create\`. The CLI writes the same row and skips what surrounds it — the card does not become your active card, and the design gate then refuses every edit you make afterwards.`,
|
|
323
|
+
`## Keep it findable`,
|
|
324
|
+
`A workspace nobody can navigate is the same as an empty one.`,
|
|
325
|
+
`- **One project per body of work.** Everything for it filed there.`,
|
|
326
|
+
`- **Say what a thing is in its title.** "PRD — Checkout redesign", "Eng design — Checkout redesign", "Sprint 4 plan". A document called "Notes" is lost the day after you write it.`,
|
|
327
|
+
`- **Link, do not copy.** A PRD references the design doc; the design doc references the tickets. \`document-link-resolve\` makes a real link that survives a rename — pasting a title makes a screenshot that quietly goes stale.`,
|
|
328
|
+
`- **Update, do not accumulate.** When a decision changes, edit the document and note what changed. A second PRD beside the first means nobody knows which one is true.`,
|
|
329
|
+
`## When the tooling is the problem, file it`,
|
|
330
|
+
`You will hit things the workspace cannot do, or does badly — a verb that fails silently, an argument nobody documented, a missing capability that forced a workaround, a step that took six calls and should have taken one.`,
|
|
331
|
+
`**File it with \`beehaven call dev-report\`** — \`kind: "bug"\` when something lies or loses work, \`kind: "request"\` when it is merely missing. Say what you TRIED, what you GOT, what you EXPECTED, and the verb involved. Check \`beehaven call dev-report '{"list":true}'\` first so you are not filing the same thing twice.`,
|
|
332
|
+
`⚠ **File it and CARRY ON.** A tooling gap is not a reason to stop the work; it is a note for whoever maintains the tools. Do not narrate it to the stakeholder unless it changed what you delivered — then one clause, saying what you did instead.`,
|
|
333
|
+
`⚠ **Report the gap, not a feature request for yourself.** "\`document-create\` silently drops \`content\`" is useful. "It would be nice if the workspace were better" is not.`,
|
|
334
|
+
`## What to say back`,
|
|
335
|
+
`The stakeholder does not want a manifest. They want to know what happened and what is next.`,
|
|
336
|
+
`Two or three sentences. What you produced, where it is, and the one thing you need from them if there is one. **Name the artifacts by their key or title, never by uuid.** "PRD and eng design are in Checkout Redesign, six tickets on Sprint 4, first three in progress" — not a list of ids, and not a paragraph about your process.`,
|
|
337
|
+
`⚠ **Do not narrate the ceremony.** They should see the workspace fill up, not read a description of a company pretending to have meetings.`,
|
|
338
|
+
].join('\n\n');
|