beehaven 0.8.34 → 0.8.36

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.
@@ -14,6 +14,7 @@ import { writeFileSync, readFileSync, renameSync, chmodSync, mkdirSync, existsSy
14
14
  import { createHash } from 'node:crypto';
15
15
  import { join } from 'node:path';
16
16
  import { homedir } from 'node:os';
17
+ import { ReconnectBackoff } from './reconnect-backoff.js';
17
18
  const CONFIG_DIR = join(homedir(), '.beehaven');
18
19
  function scopedPresenceFile(scope) {
19
20
  const digest = createHash('sha256').update(String(scope || 'local')).digest('hex').slice(0, 24);
@@ -31,8 +32,6 @@ export class AgentSessions {
31
32
  presenceBroadcast = null;
32
33
  /** A desktop identity is presence, not a permanent login badge. */
33
34
  static IDLE_TIMEOUT_MS = 30 * 60 * 1000;
34
- static RECONNECT_MIN_MS = 1_000;
35
- static RECONNECT_MAX_MS = 30_000;
36
35
  /** @param wsBase returns the relay ws base, e.g. wss://…/ (no trailing path) */
37
36
  constructor(wsBase, presenceScope = 'local') {
38
37
  this.wsBase = wsBase;
@@ -188,8 +187,7 @@ export class AgentSessions {
188
187
  scheduleReconnect(agentId, spec) {
189
188
  if (this.specs.get(agentId) !== spec || spec.reconnectTimer)
190
189
  return;
191
- const delay = spec.reconnectDelay;
192
- spec.reconnectDelay = Math.min(spec.reconnectDelay * 2, AgentSessions.RECONNECT_MAX_MS);
190
+ const delay = spec.backoff.next();
193
191
  spec.reconnectTimer = setTimeout(() => {
194
192
  spec.reconnectTimer = null;
195
193
  this.openSocket(agentId, spec);
@@ -213,6 +211,7 @@ export class AgentSessions {
213
211
  const url = `${base}/my/workspace/ws?token=${encodeURIComponent(spec.token)}&clientType=agent&client=agent&deviceId=agent-${encodeURIComponent(agentId)}`;
214
212
  let ws;
215
213
  let opened = false;
214
+ let openedAt = null;
216
215
  let settled = false;
217
216
  let timer = null;
218
217
  const settleFirst = (result) => {
@@ -253,7 +252,10 @@ export class AgentSessions {
253
252
  // Reconnecting transport is not agent activity. A new socket stays inactive until a
254
253
  // verified call touches the identity; a persisted interaction keeps its existing clock.
255
254
  this.scheduleIdleExpiry(agentId);
256
- spec.reconnectDelay = AgentSessions.RECONNECT_MIN_MS;
255
+ // ⚠ PPO-532: opening is not recovering — this reset-on-open made a workspace that drops
256
+ // every new socket while hydrating (1006) see a reconnect every ~1 s, in step with the
257
+ // daemon's own socket. The backoff now resets only after the socket stays open 60 s.
258
+ openedAt = spec.backoff.opened();
257
259
  this.syncPersistedInteractions();
258
260
  this.syncActiveSessionsFile();
259
261
  settleFirst({ ok: true });
@@ -290,6 +292,7 @@ export class AgentSessions {
290
292
  return;
291
293
  }
292
294
  this.syncActiveSessionsFile();
295
+ spec.backoff.closed(openedAt);
293
296
  this.scheduleReconnect(agentId, spec);
294
297
  };
295
298
  }
@@ -300,7 +303,7 @@ export class AgentSessions {
300
303
  const spec = {
301
304
  token,
302
305
  reconnectTimer: null,
303
- reconnectDelay: AgentSessions.RECONNECT_MIN_MS,
306
+ backoff: new ReconnectBackoff(),
304
307
  };
305
308
  this.specs.set(agentId, spec);
306
309
  return new Promise((resolve) => this.openSocket(agentId, spec, resolve));
@@ -1,64 +1,100 @@
1
1
  /**
2
2
  * agents-md-text — the Clearly block for a PROJECT's AGENTS.md, as `beehaven agents-md` writes it
3
- * (CAN2-66).
3
+ * (CAN2-66, AWO-401).
4
4
  *
5
5
  * ⚠⚠ A COPY, PINNED. The source of truth is the monorepo's `lib/connections/agentsMd.ts`, which the
6
- * Integrations page, each agent's details and the MCP `clearly-agents-md` prompt render. This
7
- * package cannot import it (it ships on its own), so the text is repeated here and the monorepo's
8
- * `lib/__tests__/agentsMdCliParity.test.mjs` compares the two OUTPUTS, name argument included.
9
- * Change both or the test fails — a stale copy does not render wrong, it instructs wrong.
6
+ * Integrations page, each agent's details, the /cli and /mcp guides and the MCP `clearly-agents-md`
7
+ * prompt render. This package cannot import it (it ships on its own), so the text is repeated here
8
+ * and the monorepo's `lib/__tests__/agentsMdCliParity.test.mjs` compares the two OUTPUTS, name
9
+ * argument included. Change both or the test fails — a stale copy does not render wrong, it
10
+ * instructs wrong.
11
+ *
12
+ * ⚠ ONE TRACKED BLOCK FOR MCP AND THE CLI (founder 2026-10-28): every task is tracked in Clearly,
13
+ * and each step names the MCP tool and the CLI command. The per-route blocks before it are removed
14
+ * when this one is written (`LEGACY_BLOCK_MARKERS`).
10
15
  *
11
16
  * ⚠ NO IMPORTS, so that test can load this file directly.
12
17
  */
13
- export const CLI_BLOCK_START = '<!-- clearly:cli:start -->';
14
- export const CLI_BLOCK_END = '<!-- clearly:cli:end -->';
15
- export const MCP_BLOCK_START = '<!-- clearly:mcp:start -->';
16
- export const MCP_BLOCK_END = '<!-- clearly:mcp:end -->';
18
+ export const AGENTS_MD_START = '<!-- clearly:start -->';
19
+ export const AGENTS_MD_END = '<!-- clearly:end -->';
20
+ /** The per-route markers used before AWO-401. Writers remove these blocks; nothing writes them. */
21
+ export const LEGACY_BLOCK_MARKERS = [
22
+ ['<!-- clearly:cli:start -->', '<!-- clearly:cli:end -->'],
23
+ ['<!-- clearly:mcp:start -->', '<!-- clearly:mcp:end -->'],
24
+ ];
25
+ /**
26
+ * The opt-out (AWO-403): a project whose AGENTS.md or CLAUDE.md carries this exact line is not tracked in Clearly.
27
+ * The global block (agentsGlobal.ts) and the MCP standing instructions name it, so the opt-out is a literal an agent
28
+ * matches, not a sentence it interprets. `beehaven agents-md --not-tracked` writes it between Clearly's markers.
29
+ */
30
+ export const AGENTS_MD_OPT_OUT_LINE = 'Clearly: not tracked';
31
+ /** The opted-out project's block: the line, inside the same markers, so re-running the setup turns tracking back on. */
32
+ export function agentsMdOptOutBlock() {
33
+ return [
34
+ AGENTS_MD_START,
35
+ '<!-- managed-by: clearly. Run `beehaven agents-md` to track this project in Clearly again. -->',
36
+ '',
37
+ AGENTS_MD_OPT_OUT_LINE,
38
+ AGENTS_MD_END,
39
+ ].join('\n');
40
+ }
41
+ /** How Claude Code runs the MCP server's `clearly-agents-md` prompt, which writes the block. */
42
+ export const MCP_AGENTS_MD_SLASH = '/mcp__clearly__clearly-agents-md';
17
43
  /** What goes in CLAUDE.md so Claude Code reads the AGENTS.md block too. */
18
44
  export const CLAUDE_MD_IMPORT = '@AGENTS.md';
19
- /** The block for agents that use the `beehaven` CLI. */
20
- export function agentsMdCliBlock(agentName) {
45
+ /** Where the block goes, said once for every surface. */
46
+ export const AGENTS_MD_HOW = 'Every agent that opens the repository reads AGENTS.md, so the block makes each session track its work in Clearly. Claude Code reads CLAUDE.md — give it one line.';
47
+ /** The MCP tools the block names. Each must be on the server's `tools/list` (a Worker test pins it). */
48
+ export const MCP_BLOCK_TOOLS = ['clearly_catalog', 'clearly_read', 'clearly_grep', 'clearly_create', 'clearly_edit', 'clearly_comment'];
49
+ /**
50
+ * The block. `agentName` personalises the CLI sign-in line for one agent (its details view);
51
+ * without it the agent is told to use its own name.
52
+ */
53
+ export function agentsMdBlock(agentName) {
21
54
  const name = (agentName ?? '').trim();
22
55
  const login = name
23
- ? `\`beehaven agent login ${name} --label "<what you are doing>"\` — you are ${name} in this workspace.`
24
- : '`beehaven agent login <name> --label "<what you are doing>"`, using your own name (claude, codex, gemini).';
56
+ ? `\`beehaven agent login ${name} --label "<the task>"\` — you are ${name} in this workspace —`
57
+ : '`beehaven agent login <your-name> --label "<the task>"` (claude, codex, gemini…)';
25
58
  return [
26
- CLI_BLOCK_START,
27
- '<!-- managed-by: clearly. Run `beehaven agents-md` to update; edit outside this block. -->',
59
+ AGENTS_MD_START,
60
+ '<!-- managed-by: clearly. Run `beehaven agents-md` or the `clearly-agents-md` MCP prompt to update; edit outside this block. -->',
61
+ '',
62
+ '## Clearly workspace: track every task',
28
63
  '',
29
- '## Clearly workspace',
64
+ "This project's plans, tickets, documents and designs live in Clearly. Track every task there,",
65
+ 'even when the person does not ask. Use whichever route this session has: the `clearly_*` MCP tools,',
66
+ "or the `beehaven` CLI (`beehaven actions` lists actions; `beehaven call <action> '<json>'` runs one).",
30
67
  '',
31
- 'This project keeps its documents, plans, boards and tickets in Clearly. Use the `beehaven` CLI',
32
- 'for anything the team should see, not files in this repo.',
68
+ '### Start of every session',
33
69
  '',
34
- `- Sign in once per session: ${login}`,
70
+ `- CLI: sign in first with ${login} and read the brief it prints.`,
35
71
  ' If it says the daemon is not running: `beehaven start --headless`.',
36
- '- Read the `clearly-agent` skill before anything else, and `clearly-canvas` before design work',
37
- ' (`beehaven skills install` puts them in place).',
38
- '- `beehaven actions` lists what you can call; `beehaven call <action> \'<json>\'` calls it.',
39
- '- Search before you create, file work in the project it belongs to, and end the session with',
40
- ' `beehaven agent handoff "what you did · what is next"` so the next one starts there.',
41
- CLI_BLOCK_END,
42
- ].join('\n');
43
- }
44
- /** The block for agents connected over MCP (the `clearly` server). */
45
- export function agentsMdMcpBlock() {
46
- return [
47
- MCP_BLOCK_START,
48
- '<!-- managed-by: clearly-mcp. Re-run the `clearly-agents-md` prompt to update; edit outside this block. -->',
72
+ ' MCP: you are already signed in as the agent chosen when Clearly was connected.',
73
+ "- Read this project's open tickets and its latest handoff before starting (`clearly_grep` /",
74
+ ' `clearly_read`, or the brief).',
75
+ '',
76
+ '### Every task has a ticket',
77
+ '',
78
+ '1. Search first (`clearly_grep`, `clearly_catalog`, or `beehaven call ticket-list`). Reuse the project,',
79
+ ' board and ticket that already own the work; create a board only if the project has none.',
80
+ '2. Create the ticket before changing code or files (`clearly_create` with `type: "ticket"` and',
81
+ ' `fields.boardId`, or `beehaven call ticket-create`). Say in its body what done means.',
82
+ '3. Move it to in-progress when work begins (`clearly_edit`, or `beehaven call ticket-update`).',
83
+ '4. File what you make (documents, designs, notes) into the project and link it from the ticket.',
84
+ ' Record decisions, open questions and progress as comments (`clearly_comment`).',
85
+ '5. Mark it done only after verifying the result, with the resolution and any remaining risk.',
86
+ '',
87
+ 'Questions and reads need no ticket.',
49
88
  '',
50
- '## Clearly workspace (connected over MCP as `clearly`)',
89
+ '### End of every session',
51
90
  '',
52
- 'This project keeps its documents, plans, boards and tickets in Clearly. Use the `clearly_*`',
53
- 'tools, not files in this repo, for anything the team should see.',
91
+ '- Leave a handoff: what you did and what is next. CLI: `beehaven agent handoff "<done · next>"`.',
92
+ ' MCP: a comment on the work ticket.',
54
93
  '',
55
- '- Start with `clearly_catalog` to see what exists; search with `clearly_grep` / `clearly_glob`.',
56
- '- Read with `clearly_read`, make new work with `clearly_create`, change it with `clearly_edit`,',
57
- ' and discuss it with `clearly_comment`. They take a `target`: a path (`~/Q1/Plan.md`), a key',
58
- ' (`CLR-42`), an id or an exact title. The extension names the type (`.md` document, `.csv`',
59
- ' sheet, `.board.json` board); a trailing slash means a project.',
60
- '- Search before you write, and write decisions back so the next session inherits them.',
61
- '- If a job is not covered by a listed tool, say so rather than guessing an action name.',
62
- MCP_BLOCK_END,
94
+ 'If this session has neither the `clearly_*` tools nor `beehaven`, stop and ask the person to connect',
95
+ 'Clearly (clearly.sh/docs/mcp or clearly.sh/docs/cli). Do not skip the tracking. In CI or a cloud',
96
+ 'agent, where nobody can sign in, connect over MCP with an agent token from the agent\'s details in',
97
+ 'Clearly, kept as a secret.',
98
+ AGENTS_MD_END,
63
99
  ].join('\n');
64
100
  }
@@ -291,3 +291,97 @@ export async function browserLogin() {
291
291
  persistBrowserLogin(captured);
292
292
  return captured;
293
293
  }
294
+ /** The relay's HTTPS origin for the configured env (prod when unconfigured). */
295
+ export function relayHttpFromConfig(cfg = readConfig(), env = process.env) {
296
+ const override = env.BEEHAVEN_RELAY_HTTP_URL;
297
+ if (override)
298
+ return override.replace(/\/+$/, '');
299
+ return resolveRelaySelection(cfg).url.replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://').replace(/\/+$/, '');
300
+ }
301
+ /**
302
+ * True where a browser on this machine is unlikely: an SSH session, CI, or Linux with no display.
303
+ * `beehaven login --browser` overrides it; `--device-code` forces the device flow anywhere.
304
+ */
305
+ export function preferDeviceLogin(env = process.env, platform = process.platform) {
306
+ if (env.SSH_CONNECTION || env.SSH_TTY || env.CI)
307
+ return true;
308
+ return platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY;
309
+ }
310
+ /** The approval page, optionally with the code filled in. */
311
+ export function deviceAuthUrl(authOrigin, userCode) {
312
+ return withUtm(`${authOrigin}/cli/device${userCode ? `?code=${encodeURIComponent(userCode)}` : ''}`);
313
+ }
314
+ /** Email and name from the ID token's payload, for the "Signed in as" line. Display only. */
315
+ function idTokenClaims(idToken) {
316
+ try {
317
+ const payload = JSON.parse(Buffer.from(idToken.split('.')[1] ?? '', 'base64url').toString('utf8'));
318
+ return { email: typeof payload.email === 'string' ? payload.email : undefined, name: typeof payload.name === 'string' ? payload.name : undefined };
319
+ }
320
+ catch {
321
+ return {};
322
+ }
323
+ }
324
+ export function captureDeviceLogin(opts) {
325
+ const { relayOrigin, authOrigin, client, fetchImpl = fetch, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now, log = (line) => console.log(line), } = opts;
326
+ const post = async (path, body) => {
327
+ const res = await fetchImpl(`${relayOrigin}${path}`, {
328
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
329
+ });
330
+ const json = await res.json().catch(() => ({}));
331
+ return { res, json };
332
+ };
333
+ return (async () => {
334
+ let start;
335
+ try {
336
+ const { res, json } = await post('/cli/device/start', { client });
337
+ if (!res.ok || !json?.ok)
338
+ throw new Error(json?.error === 'slow_down' ? 'Too many sign-in attempts from here. Wait a while and try again.' : (json?.error || `relay answered ${res.status}`));
339
+ start = json;
340
+ }
341
+ catch (err) {
342
+ throw new Error(`Couldn't start a device sign-in: ${err?.message ?? err}`);
343
+ }
344
+ log('');
345
+ log(` On any device with a browser, open ${color.cyan(deviceAuthUrl(authOrigin))}`);
346
+ log(` and enter this code: ${color.bold(start.userCode)}`);
347
+ log('');
348
+ log(color.dim(` Direct link: ${deviceAuthUrl(authOrigin, start.userCode)}`));
349
+ log(color.dim(` Waiting for approval (the code expires in ${Math.round(start.expiresIn / 60)} minutes)…`));
350
+ let interval = Math.max(1, start.interval) * 1000;
351
+ const deadline = now() + start.expiresIn * 1000;
352
+ while (now() < deadline) {
353
+ await sleep(interval);
354
+ let res;
355
+ let json;
356
+ try {
357
+ ({ res, json } = await post('/cli/device/token', { deviceCode: start.deviceCode }));
358
+ }
359
+ catch {
360
+ continue; // a dropped poll is retried at the next tick
361
+ }
362
+ if (res.ok && json?.ok && json.idToken && json.refreshToken && json.userId) {
363
+ const { email, name } = idTokenClaims(json.idToken);
364
+ return { idToken: json.idToken, refreshToken: json.refreshToken, userId: json.userId, email, displayName: name };
365
+ }
366
+ switch (json?.error) {
367
+ case 'authorization_pending': continue;
368
+ case 'slow_down':
369
+ interval += 5000;
370
+ continue;
371
+ case 'access_denied': throw new Error('The sign-in was denied in the browser.');
372
+ case 'expired_token': throw new Error('The code expired. Run `beehaven login --device-code` again.');
373
+ default:
374
+ if (res.status >= 500)
375
+ throw new Error(json?.error || `relay answered ${res.status}`);
376
+ }
377
+ }
378
+ throw new Error('The code expired. Run `beehaven login --device-code` again.');
379
+ })();
380
+ }
381
+ /** Device-code sign-in end to end: start, show the code, poll, persist like the browser flow. */
382
+ export async function deviceLogin(opts = {}) {
383
+ const { authOrigin } = resolveAuthOrigin();
384
+ const captured = await captureDeviceLogin({ relayOrigin: relayHttpFromConfig(), authOrigin, client: opts.client });
385
+ persistBrowserLogin(captured);
386
+ return captured;
387
+ }
@@ -53,9 +53,9 @@ export const PUBLIC_BUILD = true;
53
53
  * ⚠ REWRITTEN IN `dist/` AT BUILD TIME, like `PUBLIC_BUILD`. `unstamped` is what a source checkout
54
54
  * carries and what `tsx`/`vitest` see, and it is honest: nothing has stamped them.
55
55
  */
56
- export const BUILD_SHA = '0fc3f190acdb';
56
+ export const BUILD_SHA = '31746b91dfbf';
57
57
  /** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
58
- export const BUILD_TIME = '2026-10-27T20:29Z';
58
+ export const BUILD_TIME = '2026-10-29T05:50Z';
59
59
  /**
60
60
  * The build identity as one short string, for `--version`.
61
61
  *
package/dist/cli.js CHANGED
@@ -284,6 +284,7 @@ ${DEV_ONLY_HELP ? ` PROJECT — build your own Durable Object backend (HIVE_DE
284
284
  mcp status Which clients are configured
285
285
  mcp remove Remove the entries + revoke the token
286
286
  mcp-config [--name N] Print the block instead of writing it
287
+ mcp-config --token-only --expires <days> Print ONLY the token, for a secret store
287
288
 
288
289
  Anywhere else — claude.ai, Claude Desktop, another machine — point the client
289
290
  at the endpoint and let it log you in; nothing to install and no token to paste:
@@ -1340,7 +1341,7 @@ async function run() {
1340
1341
  state: { type: 'string' },
1341
1342
  goal: { type: 'string' },
1342
1343
  remove: { type: 'boolean', default: false },
1343
- // `agents-md`: the MCP block instead of the CLI one; print it instead of writing it (CAN2-66).
1344
+ // `agents-md`: `--mcp` is accepted for older instructions (one block since AWO-401); `--print` prints it.
1344
1345
  mcp: { type: 'boolean', default: false },
1345
1346
  print: { type: 'boolean', default: false },
1346
1347
  /**
@@ -1417,6 +1418,10 @@ async function run() {
1417
1418
  // (a live client) when none is connected; `--dev` → localhost.
1418
1419
  launch: { type: 'boolean', default: false },
1419
1420
  browser: { type: 'boolean', default: false },
1421
+ // `beehaven login --device-code` — sign in by approving a code on another device (AWO-402).
1422
+ 'device-code': { type: 'boolean', default: false },
1423
+ // `beehaven agents-md --not-tracked` — write the opt-out line instead of the tracked block (AWO-403).
1424
+ 'not-tracked': { type: 'boolean', default: false },
1420
1425
  dev: { type: 'boolean', default: false },
1421
1426
  // `beehaven canvas perceive --pixels --max 80`, `canvas verify --keep`
1422
1427
  pixels: { type: 'boolean', default: false },
@@ -1461,6 +1466,8 @@ async function run() {
1461
1466
  client: { type: 'string' },
1462
1467
  /** `beehaven mcp install --rotate` — mint a new token instead of reusing the installed one. */
1463
1468
  rotate: { type: 'boolean' },
1469
+ /** `beehaven mcp-config --token-only --expires <days>` — the raw token alone on stdout, for piping. */
1470
+ 'token-only': { type: 'boolean' },
1464
1471
  password: { type: 'string' },
1465
1472
  email: { type: 'string' },
1466
1473
  expires: { type: 'string' },
@@ -3033,10 +3040,19 @@ async function run() {
3033
3040
  // own copy aimed at /auth/beehaven, a route the web app no longer serves.
3034
3041
  // What follows the sign-in — restarting the daemon, teaching the agents —
3035
3042
  // stays here, because the daemon fallback must not restart itself.
3043
+ // AWO-402 — the device-code flow for a machine with no browser of its own: forced with
3044
+ // `--device-code`, chosen automatically over SSH / CI / a display-less Linux, and never when
3045
+ // the person asked for `--browser`.
3036
3046
  let captured;
3037
3047
  try {
3038
- const { browserLogin } = await import('./browser-login.js');
3039
- captured = await browserLogin();
3048
+ const { browserLogin, deviceLogin, preferDeviceLogin } = await import('./browser-login.js');
3049
+ const useDevice = Boolean(values['device-code']) || (!values.browser && preferDeviceLogin());
3050
+ if (useDevice && !values['device-code']) {
3051
+ console.log(color.dim(' No browser on this machine, so signing in with a code (`beehaven login --browser` to open one here).'));
3052
+ }
3053
+ captured = useDevice
3054
+ ? await deviceLogin({ client: `beehaven ${getVersion()} (${process.platform})` })
3055
+ : await browserLogin();
3040
3056
  }
3041
3057
  catch (err) {
3042
3058
  console.error(` ${color.red('✗')} ${err?.message ?? err}`);
@@ -3170,6 +3186,7 @@ async function run() {
3170
3186
  mcp: !!values.mcp,
3171
3187
  print: !!values.print,
3172
3188
  remove: !!values.remove,
3189
+ notTracked: Boolean(values['not-tracked']),
3173
3190
  });
3174
3191
  break;
3175
3192
  }
@@ -4073,10 +4090,82 @@ complete -F _beehaven_completions beehaven`);
4073
4090
  const tgtAddress = tgt?.address ?? 'home';
4074
4091
  const mcpEntry = entryNameFor(tgtAddress);
4075
4092
  const wsId = isHomeAddress(tgtAddress) ? '' : tgtAddress.split('/').slice(1).join('/');
4076
- const label = values.name || `${mcpEntry} · ${new Date().toISOString().slice(0, 10)}`;
4093
+ const defaultMcpLabel = `${mcpEntry} · ${new Date().toISOString().slice(0, 10)}`;
4077
4094
  const { rpcOnce: _rpc } = await import('./ipc-client.js');
4078
4095
  const { resolveActiveAgent } = await import('./agent-state.js');
4096
+ const { mcpMintInput, runMcpTokenOnly } = await import('./commands/mcp-token.js');
4079
4097
  const activeMcpAgent = resolveActiveAgent(values);
4098
+ /**
4099
+ * ⚠⚠ DO NOT ECHO THE RAW ERROR — IT SENT PEOPLE TO THE WRONG FIX.
4100
+ *
4101
+ * Observed: this printed "Cannot connect to daemon. Run `beehaven start --headless`" in the
4102
+ * same second that `beehaven status` reported "Daemon: running". The daemon was up and the
4103
+ * RELAY had dropped. Following the advice runs into the single-instance guard, which
4104
+ * refuses — so the message costs a cycle and teaches the wrong model of the failure.
4105
+ *
4106
+ * The CLI already distinguishes these three states carefully (see `status`), precisely
4107
+ * because the fixes are mutually exclusive: a restart will NOT refresh an expired token,
4108
+ * and a re-login will NOT revive a dropped socket. This path bypassed all of it by
4109
+ * forwarding whatever string the transport happened to raise.
4110
+ *
4111
+ * Re-probe and say what is actually true. Stderr only, and it returns the exit code, so the
4112
+ * `--token-only` path can use it without touching stdout.
4113
+ */
4114
+ const explainMintFailure = async (err) => {
4115
+ const { isDaemonRunning } = await import('./daemon-client.js');
4116
+ let up = false;
4117
+ try {
4118
+ up = await isDaemonRunning();
4119
+ }
4120
+ catch {
4121
+ up = false;
4122
+ }
4123
+ let expired = false;
4124
+ try {
4125
+ const cfg = JSON.parse(readFileSync(join(homedir(), '.beehaven', 'config.json'), 'utf8'));
4126
+ const payload = JSON.parse(Buffer.from(String(cfg.token).split('.')[1], 'base64').toString());
4127
+ expired = !payload.exp || Date.now() / 1000 > payload.exp - 60;
4128
+ }
4129
+ catch {
4130
+ expired = true;
4131
+ }
4132
+ console.error(` ${color.red('✗')} Could not mint a token.`);
4133
+ console.error(!up
4134
+ ? ' The daemon is not running — `beehaven start --headless`'
4135
+ : expired
4136
+ ? ' The daemon is up but your token has EXPIRED — `beehaven login` (a restart will not refresh it)'
4137
+ : ' The daemon is up and your token is valid; the relay socket dropped — `beehaven restart`');
4138
+ console.error(color.dim(` (${err?.message ?? err})`));
4139
+ return 1;
4140
+ };
4141
+ /**
4142
+ * ⚠⚠ `--token-only`: THE RAW TOKEN ON STDOUT AND NOTHING ELSE, for
4143
+ * `… | gh secret set CLEARLY_MCP_TOKEN`. The rules (stderr for every message, `--expires`
4144
+ * required, the caller's own agent) live in commands/mcp-token.ts, where the tests call them.
4145
+ * It branches off HERE, before the reuse check (print never reuses) and before anything below
4146
+ * writes to stdout.
4147
+ */
4148
+ if (values['token-only']) {
4149
+ process.exitCode = await runMcpTokenOnly({
4150
+ sub: mcpSub,
4151
+ flags: values,
4152
+ defaultLabel: defaultMcpLabel,
4153
+ agentId: activeMcpAgent?.agentId,
4154
+ mint: (input) => _rpc('create-mcp-token', input, 30_000),
4155
+ out: (s) => { process.stdout.write(s); },
4156
+ err: (s) => { process.stderr.write(s); },
4157
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
4158
+ onMintError: explainMintFailure,
4159
+ });
4160
+ break;
4161
+ }
4162
+ // `--label` (else `--name`), `--expires <days>` and the caller's agent, for every mint below.
4163
+ const mintInput = mcpMintInput(values, { label: defaultMcpLabel, agentId: activeMcpAgent?.agentId });
4164
+ if ('error' in mintInput) {
4165
+ console.error(` ${color.red('✗')} ${mintInput.error}`);
4166
+ process.exit(2);
4167
+ }
4168
+ const label = mintInput.input.label;
4080
4169
  let res;
4081
4170
  /**
4082
4171
  * ⚠⚠ REUSE THE TOKEN ALREADY INSTALLED RATHER THAN MINTING ANOTHER. Minting
@@ -4121,54 +4210,11 @@ complete -F _beehaven_completions beehaven`);
4121
4210
  }
4122
4211
  }
4123
4212
  try {
4124
- if (!res) {
4125
- res = await _rpc('create-mcp-token', {
4126
- label,
4127
- ...(activeMcpAgent?.agentId ? { agentId: activeMcpAgent.agentId } : {}),
4128
- }, 30_000);
4129
- }
4213
+ if (!res)
4214
+ res = await _rpc('create-mcp-token', mintInput.input, 30_000);
4130
4215
  }
4131
4216
  catch (err) {
4132
- /**
4133
- * ⚠⚠ DO NOT ECHO THE RAW ERROR — IT SENT PEOPLE TO THE WRONG FIX.
4134
- *
4135
- * Observed: this printed "Cannot connect to daemon. Run `beehaven start --headless`" in the
4136
- * same second that `beehaven status` reported "Daemon: running". The daemon was up and the
4137
- * RELAY had dropped. Following the advice runs into the single-instance guard, which
4138
- * refuses — so the message costs a cycle and teaches the wrong model of the failure.
4139
- *
4140
- * The CLI already distinguishes these three states carefully (see `status`), precisely
4141
- * because the fixes are mutually exclusive: a restart will NOT refresh an expired token,
4142
- * and a re-login will NOT revive a dropped socket. This path bypassed all of it by
4143
- * forwarding whatever string the transport happened to raise.
4144
- *
4145
- * Re-probe and say what is actually true.
4146
- */
4147
- const { isDaemonRunning } = await import('./daemon-client.js');
4148
- let up = false;
4149
- try {
4150
- up = await isDaemonRunning();
4151
- }
4152
- catch {
4153
- up = false;
4154
- }
4155
- let expired = false;
4156
- try {
4157
- const cfg = JSON.parse(readFileSync(join(homedir(), '.beehaven', 'config.json'), 'utf8'));
4158
- const payload = JSON.parse(Buffer.from(String(cfg.token).split('.')[1], 'base64').toString());
4159
- expired = !payload.exp || Date.now() / 1000 > payload.exp - 60;
4160
- }
4161
- catch {
4162
- expired = true;
4163
- }
4164
- console.error(` ${color.red('✗')} Could not mint a token.`);
4165
- console.error(!up
4166
- ? ' The daemon is not running — `beehaven start --headless`'
4167
- : expired
4168
- ? ' The daemon is up but your token has EXPIRED — `beehaven login` (a restart will not refresh it)'
4169
- : ' The daemon is up and your token is valid; the relay socket dropped — `beehaven restart`');
4170
- console.error(color.dim(` (${err?.message ?? err})`));
4171
- process.exit(1);
4217
+ process.exit(await explainMintFailure(err));
4172
4218
  }
4173
4219
  if (!res?.key) {
4174
4220
  console.error(` ${color.red('✗')} ${res?.error ?? 'No token returned.'}`);
@@ -14,9 +14,12 @@
14
14
  * means both converge and either uninstaller removes it. `agentInstructionParity.test.mjs` in the
15
15
  * root repo pins the two copies together.
16
16
  *
17
- * ⚠ CLAUDE GETS THE SHORT POINTER — `~/.claude/CLAUDE.md` is prepended to every Claude Code
18
- * session in every repository, so it carries a pointer to the skills, not a manual. Same rule as
19
- * the Mac app; see its note on `AGENT_INSTRUCTION_FILES`.
17
+ * ⚠ ONE TRACKED BLOCK FOR EVERY AGENT, MCP OR CLI (founder 2026-10-28, AWO-401 follow-up: anyone
18
+ * using Clearly gets the mandatory block). It is short (~15 lines) because `~/.claude/CLAUDE.md` is
19
+ * prepended to every Claude Code session in every repository; the exact verbs stay in the skills.
20
+ * It has an escape hatch — a project's AGENTS.md or the person can say a project is not tracked —
21
+ * and a project's own Clearly block takes precedence. The web copy for pasting (Cursor's user
22
+ * rules) is the monorepo's lib/connections/agentsGlobal.ts, pinned to this one by a test.
20
23
  *
21
24
  * ⚠ ONLY OUR BLOCK IS EVER TOUCHED. Text around it is preserved byte for byte; a file with no
22
25
  * block gets one appended; removal deletes the block and nothing else.
@@ -41,34 +44,24 @@ export function instructionBlock() {
41
44
  BLOCK_START,
42
45
  `<!-- ${MANAGED_MARKER}. Edits inside this block are overwritten on app launch; edit outside it. -->`,
43
46
  '',
44
- '## Clearly — drive it with the `beehaven` CLI',
47
+ '## Clearly workspace: track your work there',
45
48
  '',
46
- 'Clearly (clearly.sh) is a workspace — documents, projects, boards, tickets and a design',
47
- 'canvas. Reach it from the shell with the `beehaven` CLI, NOT an HTTP API you construct.',
49
+ 'This person keeps their plans, tickets, documents and designs in Clearly (clearly.sh). Track every',
50
+ 'task there, even unasked and in any project, unless the project\'s AGENTS.md or CLAUDE.md has the line',
51
+ '`Clearly: not tracked`, or the person says not to track it. A project\'s own Clearly block in AGENTS.md',
52
+ 'takes precedence over this one.',
48
53
  '',
49
- '⚠ SIGN IN FIRST. Every call is attributed to an agent identity, and without one the CLI',
50
- 'refuses. `login` MINTS the name if it does not exist yet, and binds this terminal:',
54
+ 'Use whichever route this session has: the `clearly_*` MCP tools, or the `beehaven` CLI.',
51
55
  '',
52
- '```sh',
53
- 'beehaven agent login <your-name> # required — mints + binds, once per session',
54
- 'beehaven connect home # target the personal workspace',
55
- 'beehaven actions # list every action available there',
56
- 'beehaven call <action> \'{...}\'',
57
- '```',
56
+ '- Start. CLI: `beehaven agent login <your-name> --label "<the task>"` (claude, codex, gemini…), then',
57
+ ' read the brief it prints; if the daemon is not running, `beehaven start --headless`. MCP: you are',
58
+ ' already signed in.',
59
+ '- Each task: search first, then create a ticket before changing code or files and move it to',
60
+ ' in-progress. File what you make into the project, record decisions as comments, and mark it done',
61
+ ' only after verifying. Questions and reads need no ticket.',
62
+ '- End: a handoff, `beehaven agent handoff "<done · next>"` on the CLI or a comment on the work ticket.',
58
63
  '',
59
- 'Read the full skills before driving it — they carry the exact verbs and node shapes:',
60
- '',
61
- '- `~/.<agent>/skills/clearly-agent/SKILL.md` — **read this first**: identity, sessions, your brief',
62
- '- `~/.<agent>/skills/beehaven-cli/SKILL.md` — connect / call / sql / logs, the daemon',
63
- '- `~/.<agent>/skills/clearly-workspace/SKILL.md` — audit what a workspace holds and who made it',
64
- '- `~/.<agent>/skills/clearly-canvas/SKILL.md` — build + perceive a canvas, comm blocks, code diffs',
65
- '- `~/.<agent>/skills/design-craft/SKILL.md` — **load before any visual work**: grid, type, colour, the studio pass',
66
- '- `~/.<agent>/skills/layout-systems/SKILL.md` — grids and systems across many artboards',
67
- '- `~/.<agent>/skills/pair-on-canvas/SKILL.md` — drive a coding task from a spec on the canvas',
68
- '- `~/.<agent>/skills/codebase-map/SKILL.md` — draw a repo’s architecture on the canvas',
69
- '',
70
- 'If a command reports the daemon is unreachable, `beehaven login` re-auths and',
71
- '`beehaven start --headless` starts a daemon when the Mac app is not running.',
64
+ 'Detail is in the `clearly-agent` skill (`beehaven skills install` puts the skills in place).',
72
65
  BLOCK_END,
73
66
  ].join('\n');
74
67
  }
@@ -78,9 +71,24 @@ export function claudeBlock() {
78
71
  BLOCK_START,
79
72
  `<!-- ${MANAGED_MARKER}. Edits inside this block are overwritten on app launch; edit outside it. -->`,
80
73
  '',
81
- '**Clearly workspace** — drive it from the shell with the `beehaven` CLI (documents, projects,',
82
- 'boards, tickets, canvas). Sign in once per session first: `beehaven agent login <name>` —',
83
- 'it mints the identity if new. Full detail is in the skills, `clearly-agent` first.',
74
+ '## Clearly workspace: track your work there',
75
+ '',
76
+ 'This person keeps their plans, tickets, documents and designs in Clearly (clearly.sh). Track every',
77
+ 'task there, even unasked and in any project, unless the project\'s AGENTS.md or CLAUDE.md has the line',
78
+ '`Clearly: not tracked`, or the person says not to track it. A project\'s own Clearly block in AGENTS.md',
79
+ 'takes precedence over this one.',
80
+ '',
81
+ 'Use whichever route this session has: the `clearly_*` MCP tools, or the `beehaven` CLI.',
82
+ '',
83
+ '- Start. CLI: `beehaven agent login <your-name> --label "<the task>"` (claude, codex, gemini…), then',
84
+ ' read the brief it prints; if the daemon is not running, `beehaven start --headless`. MCP: you are',
85
+ ' already signed in.',
86
+ '- Each task: search first, then create a ticket before changing code or files and move it to',
87
+ ' in-progress. File what you make into the project, record decisions as comments, and mark it done',
88
+ ' only after verifying. Questions and reads need no ticket.',
89
+ '- End: a handoff, `beehaven agent handoff "<done · next>"` on the CLI or a comment on the work ticket.',
90
+ '',
91
+ 'Detail is in the `clearly-agent` skill (`beehaven skills install` puts the skills in place).',
84
92
  BLOCK_END,
85
93
  ].join('\n');
86
94
  }