beehaven 0.8.27 → 0.8.33
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/dist/agents-md-text.js +64 -0
- package/dist/browser-login.js +14 -4
- package/dist/build-channel.js +2 -2
- package/dist/claude-bridge.js +12 -9
- package/dist/clearly.js +6 -4
- package/dist/cli.js +27 -8
- package/dist/commands/agents-md.js +118 -0
- package/dist/commands/daemon-cmd.js +2 -0
- package/dist/commands/help.js +21 -0
- package/dist/commands/self-update.js +32 -5
- package/dist/commands/workspace.js +22 -5
- package/dist/daemon-client.js +10 -6
- package/dist/daemon-entry.js +1 -0
- package/dist/daemon-process.js +125 -0
- package/dist/daemon.js +131 -62
- package/dist/dashboard.js +118 -6
- package/dist/ipc-auth.js +3 -1
- package/dist/ipc-client.js +7 -6
- package/dist/ipc-path.js +92 -0
- package/dist/ipc-server.js +33 -33
- package/dist/npm-update.js +138 -0
- package/dist/relay-presets.js +3 -4
- package/dist/setup-service.js +9 -4
- package/dist/verb-surface.js +1 -0
- package/dist/watcher.js +9 -23
- package/dist/windows-hide.js +77 -0
- package/package.json +1 -1
- package/scripts/sync-skills.mjs +6 -0
- package/skills/agent-doodle-space/SKILL.md +1 -1
- package/skills/agent-home/SKILL.md +1 -1
- package/skills/beehaven-cli/SKILL.md +12 -0
- package/skills/brand-identity/SKILL.md +37 -14
- package/skills/clearly-agent/SKILL.md +24 -4
- package/skills/clearly-canvas/SKILL.md +88 -11
- package/skills/clearly-canvas/authoring.md +61 -10
- package/skills/clearly-canvas/example.mjs +72 -69
- package/skills/clearly-canvas/review.mjs +106 -12
- package/skills/clearly-docs/SKILL.md +151 -0
- package/skills/clearly-init/SKILL.md +135 -0
- package/skills/clearly-workflows/SKILL.md +90 -0
- package/skills/clearly-workspace/SKILL.md +1 -1
- package/skills/codebase-map/SKILL.md +1 -1
- package/skills/design-craft/SKILL.md +14 -6
- package/skills/design-craft/references.md +95 -0
- package/skills/index.json +48 -12
- package/skills/layout-systems/SKILL.md +14 -11
- package/skills/pair-on-canvas/SKILL.md +1 -1
- package/skills/sticker-pack/SKILL.md +132 -0
- package/skills/visualize/SKILL.md +188 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents-md-text — the Clearly block for a PROJECT's AGENTS.md, as `beehaven agents-md` writes it
|
|
3
|
+
* (CAN2-66).
|
|
4
|
+
*
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ NO IMPORTS, so that test can load this file directly.
|
|
12
|
+
*/
|
|
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 -->';
|
|
17
|
+
/** What goes in CLAUDE.md so Claude Code reads the AGENTS.md block too. */
|
|
18
|
+
export const CLAUDE_MD_IMPORT = '@AGENTS.md';
|
|
19
|
+
/** The block for agents that use the `beehaven` CLI. */
|
|
20
|
+
export function agentsMdCliBlock(agentName) {
|
|
21
|
+
const name = (agentName ?? '').trim();
|
|
22
|
+
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).';
|
|
25
|
+
return [
|
|
26
|
+
CLI_BLOCK_START,
|
|
27
|
+
'<!-- managed-by: clearly. Run `beehaven agents-md` to update; edit outside this block. -->',
|
|
28
|
+
'',
|
|
29
|
+
'## Clearly workspace',
|
|
30
|
+
'',
|
|
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.',
|
|
33
|
+
'',
|
|
34
|
+
`- Sign in once per session: ${login}`,
|
|
35
|
+
' 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. -->',
|
|
49
|
+
'',
|
|
50
|
+
'## Clearly workspace (connected over MCP as `clearly`)',
|
|
51
|
+
'',
|
|
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.',
|
|
54
|
+
'',
|
|
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,
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
package/dist/browser-login.js
CHANGED
|
@@ -93,10 +93,15 @@ async function warnIfLocalDevDown() {
|
|
|
93
93
|
* Linux; start on Windows. Failure is SURFACED — silent failure was the bug — so the person can
|
|
94
94
|
* paste the URL themselves.
|
|
95
95
|
*/
|
|
96
|
-
function openInBrowser(url) {
|
|
96
|
+
export function openInBrowser(url) {
|
|
97
97
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// ⚠ Windows: `start` is a cmd.exe BUILTIN, not a program, so spawning it always failed (ENOENT)
|
|
99
|
+
// and login fell back to "paste this URL". The URL protocol handler is a real executable, and
|
|
100
|
+
// passing the URL as an argv entry keeps cmd from splitting it at `&` (CAN2-56).
|
|
101
|
+
const [opener, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
102
|
+
: process.platform === 'win32' ? ['rundll32.exe', ['url.dll,FileProtocolHandler', url]]
|
|
103
|
+
: ['xdg-open', [url]];
|
|
104
|
+
const proc = spawn(opener, args, { stdio: 'ignore', detached: true, windowsHide: true });
|
|
100
105
|
proc.unref();
|
|
101
106
|
proc.on('error', (err) => {
|
|
102
107
|
console.log(` ${color.yellow('!')} Couldn't auto-open browser: ${err.message}`);
|
|
@@ -260,7 +265,12 @@ export function persistBrowserLogin(captured) {
|
|
|
260
265
|
userId: captured.userId,
|
|
261
266
|
email: captured.email ?? null,
|
|
262
267
|
displayName: captured.displayName ?? null,
|
|
263
|
-
|
|
268
|
+
/**
|
|
269
|
+
* ⚠ SAY WHERE THE TOKEN REALLY IS (CAN2-65). This was always 'keychain', so on Windows —
|
|
270
|
+
* which has no keychain here — `whoami` reported a keychain that did not exist, and
|
|
271
|
+
* `Relay.loadConfig` / the refresh path shelled out to macOS `security` on every load.
|
|
272
|
+
*/
|
|
273
|
+
tokenStorage: keychain.available ? 'keychain' : 'file',
|
|
264
274
|
refreshToken: captured.refreshToken,
|
|
265
275
|
token: captured.idToken,
|
|
266
276
|
endpoint: dialUrl,
|
package/dist/build-channel.js
CHANGED
|
@@ -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 = '
|
|
56
|
+
export const BUILD_SHA = '8f7eef9e12ce';
|
|
57
57
|
/** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
|
|
58
|
-
export const BUILD_TIME = '2026-10-
|
|
58
|
+
export const BUILD_TIME = '2026-10-15T07:24Z';
|
|
59
59
|
/**
|
|
60
60
|
* The build identity as one short string, for `--version`.
|
|
61
61
|
*
|
package/dist/claude-bridge.js
CHANGED
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
* await bridge.callApp('recorder', 'record', { source: 'mic' });
|
|
16
16
|
*/
|
|
17
17
|
import { createConnection } from 'net';
|
|
18
|
+
import { request as httpRequest } from 'http';
|
|
18
19
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
19
20
|
import { join } from 'path';
|
|
20
21
|
import { homedir } from 'os';
|
|
21
22
|
import { EventEmitter } from 'events';
|
|
22
23
|
import { activeAgentWrap } from './agent-attribution.js';
|
|
23
|
-
|
|
24
|
+
import { ipcSocketPath, watcherSocketPath, socketMayExist } from './ipc-path.js';
|
|
24
25
|
const CONFIG_DIR = join(homedir(), '.beehaven');
|
|
25
26
|
export class ClaudeBridge extends EventEmitter {
|
|
26
27
|
socket = null;
|
|
@@ -39,11 +40,12 @@ export class ClaudeBridge extends EventEmitter {
|
|
|
39
40
|
async connect() {
|
|
40
41
|
if (this.connected)
|
|
41
42
|
return true;
|
|
42
|
-
|
|
43
|
+
const ipcPath = ipcSocketPath();
|
|
44
|
+
if (!socketMayExist(ipcPath))
|
|
43
45
|
return false;
|
|
44
46
|
return new Promise((resolve) => {
|
|
45
47
|
const timeout = setTimeout(() => resolve(false), 5_000);
|
|
46
|
-
const socket = createConnection({ path:
|
|
48
|
+
const socket = createConnection({ path: ipcPath });
|
|
47
49
|
this.socket = socket;
|
|
48
50
|
socket.on('connect', () => {
|
|
49
51
|
this.connected = true;
|
|
@@ -198,11 +200,12 @@ export class ClaudeBridge extends EventEmitter {
|
|
|
198
200
|
}
|
|
199
201
|
// ── Internal ───────────────────────────────────────────────────────
|
|
200
202
|
sendEvent(hookEvent, extra = {}) {
|
|
201
|
-
// Send via the watcher socket (HTTP POST to /
|
|
202
|
-
//
|
|
203
|
-
|
|
203
|
+
// Send via the watcher socket (HTTP POST to /event) — the same protocol mwah uses.
|
|
204
|
+
// ⚠ `http` is IMPORTED: this was `require('http')`, which is undefined in this ESM package,
|
|
205
|
+
// so every event threw a ReferenceError once the socket existed (CAN2-56).
|
|
206
|
+
const watcherPath = watcherSocketPath();
|
|
207
|
+
if (!socketMayExist(watcherPath))
|
|
204
208
|
return;
|
|
205
|
-
const http = require('http');
|
|
206
209
|
const body = JSON.stringify({
|
|
207
210
|
hook_event_name: hookEvent,
|
|
208
211
|
session_id: this.sessionId,
|
|
@@ -211,8 +214,8 @@ export class ClaudeBridge extends EventEmitter {
|
|
|
211
214
|
source: 'claude-code',
|
|
212
215
|
...extra,
|
|
213
216
|
});
|
|
214
|
-
const req =
|
|
215
|
-
socketPath:
|
|
217
|
+
const req = httpRequest({
|
|
218
|
+
socketPath: watcherPath,
|
|
216
219
|
path: '/event',
|
|
217
220
|
method: 'POST',
|
|
218
221
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
package/dist/clearly.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* product promises. GitHub / Linear / Slack ingestion is a separate project; nothing here
|
|
27
27
|
* pretends otherwise, and `status` says so out loud.
|
|
28
28
|
*/
|
|
29
|
+
import './windows-hide.js'; // FIRST: no console windows flashing on Windows (CAN2-56)
|
|
29
30
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
30
31
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
31
32
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -47,11 +48,12 @@ const c = {
|
|
|
47
48
|
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
48
49
|
};
|
|
49
50
|
/**
|
|
50
|
-
* ⚠ THE AGENT
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* ⚠ THE AGENT JOB IS THE SLOW PART. The server's `RESPOND_TIMEOUT_MS` is 900s (WDP3-62);
|
|
52
|
+
* this waits that plus the report's tail, so a turn that finishes on time is never abandoned
|
|
53
|
+
* by its caller and a timeout here still means the turn itself ran out. The relay-forward window
|
|
54
|
+
* follows the `_timeout` sent with the call.
|
|
53
55
|
*/
|
|
54
|
-
const TURN_MS =
|
|
56
|
+
export const TURN_MS = 930_000;
|
|
55
57
|
/**
|
|
56
58
|
* ⚠ STDERR IS CAPTURED, NOT INHERITED. A failing probe is normal here — `HEAD~30` does not exist in
|
|
57
59
|
* a repo with two commits — and with inherited stderr git printed
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
if (!process.env.BEEHAVEN_IPC_SCOPE) {
|
|
17
17
|
process.env.BEEHAVEN_IPC_SCOPE = 'developer';
|
|
18
18
|
}
|
|
19
|
+
import './windows-hide.js'; // FIRST: no console windows flashing on Windows (CAN2-56)
|
|
19
20
|
import { parseArgs } from 'node:util';
|
|
20
21
|
import { devModeAvailable, buildLabel, PUBLIC_BUILD } from './build-channel.js';
|
|
21
22
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, } from 'fs';
|
|
@@ -142,6 +143,7 @@ function printHelp() {
|
|
|
142
143
|
|
|
143
144
|
beehaven guide Agent-first setup and signup handoff
|
|
144
145
|
beehaven signup Create a free Clearly account
|
|
146
|
+
beehaven agents-md Add Clearly to this project's AGENTS.md
|
|
145
147
|
|
|
146
148
|
Personal workspace:
|
|
147
149
|
beehaven cd home
|
|
@@ -448,8 +450,9 @@ ${DEV_ONLY_HELP ? `
|
|
|
448
450
|
|
|
449
451
|
Note: if you also run the Hive menu-bar app, the tray is the
|
|
450
452
|
primary owner of the daemon. CLI start/stop only act when the tray
|
|
451
|
-
isn't running its own daemon. The same socket (/tmp/beehaven-ipc.sock
|
|
452
|
-
is shared in both modes — never two
|
|
453
|
+
isn't running its own daemon. The same socket (/tmp/beehaven-ipc.sock;
|
|
454
|
+
a per-user named pipe on Windows) is shared in both modes — never two
|
|
455
|
+
daemons at once.
|
|
453
456
|
|
|
454
457
|
─────────────────────────────────────────────────────────────────────
|
|
455
458
|
OPTIONS
|
|
@@ -954,6 +957,11 @@ const AGENT_LOGIN_EXEMPT = new Set([
|
|
|
954
957
|
* would refuse the one person it is for — the agent whose user has not signed up yet (SGS-73).
|
|
955
958
|
*/
|
|
956
959
|
'handoff',
|
|
960
|
+
/**
|
|
961
|
+
* ⚠ `agents-md` WRITES A FILE IN THIS REPO AND NOTHING ELSE — it never opens a workspace, and it is
|
|
962
|
+
* part of setting a project up, which must not require the login it helps a teammate do (CAN2-66).
|
|
963
|
+
*/
|
|
964
|
+
'agents-md',
|
|
957
965
|
]);
|
|
958
966
|
/**
|
|
959
967
|
* ⚠ ALIAS → CANONICAL VERB, for the activity log's `source` column only.
|
|
@@ -1369,6 +1377,9 @@ async function run() {
|
|
|
1369
1377
|
state: { type: 'string' },
|
|
1370
1378
|
goal: { type: 'string' },
|
|
1371
1379
|
remove: { type: 'boolean', default: false },
|
|
1380
|
+
// `agents-md`: the MCP block instead of the CLI one; print it instead of writing it (CAN2-66).
|
|
1381
|
+
mcp: { type: 'boolean', default: false },
|
|
1382
|
+
print: { type: 'boolean', default: false },
|
|
1372
1383
|
/**
|
|
1373
1384
|
* ⚠⚠ `beehaven sheet` / `beehaven slides` — AND AN UNDECLARED FLAG IS A FATAL, NOT A NO-OP.
|
|
1374
1385
|
*
|
|
@@ -3143,6 +3154,15 @@ async function run() {
|
|
|
3143
3154
|
console.log(' (or quit and relaunch the Hive Mac app if it owns the daemon).');
|
|
3144
3155
|
break;
|
|
3145
3156
|
}
|
|
3157
|
+
case 'agents-md': {
|
|
3158
|
+
const { cmdAgentsMd } = await import('./commands/agents-md.js');
|
|
3159
|
+
process.exitCode = cmdAgentsMd(positionals[1], {
|
|
3160
|
+
mcp: !!values.mcp,
|
|
3161
|
+
print: !!values.print,
|
|
3162
|
+
remove: !!values.remove,
|
|
3163
|
+
});
|
|
3164
|
+
break;
|
|
3165
|
+
}
|
|
3146
3166
|
case 'handoff': {
|
|
3147
3167
|
const { cmdHandoff } = await import('./commands/handoff.js');
|
|
3148
3168
|
process.exitCode = await cmdHandoff(positionals[1], {
|
|
@@ -3171,11 +3191,10 @@ async function run() {
|
|
|
3171
3191
|
console.log(` Sign up at ${color.cyan(signupUrl)}.`);
|
|
3172
3192
|
console.log(' Then run "beehaven login" to wire the CLI to your account.');
|
|
3173
3193
|
console.log('');
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
catch { }
|
|
3194
|
+
// The shared opener: per-platform (Windows has no `open`), and a spawn failure prints the
|
|
3195
|
+
// URL instead of crashing on an unhandled child 'error' event.
|
|
3196
|
+
const { openInBrowser } = await import('./browser-login.js');
|
|
3197
|
+
openInBrowser(signupUrl);
|
|
3179
3198
|
break;
|
|
3180
3199
|
}
|
|
3181
3200
|
case 'treehouse': {
|
|
@@ -3917,7 +3936,7 @@ complete -F _beehaven_completions beehaven`);
|
|
|
3917
3936
|
console.log(` ${r.url}`);
|
|
3918
3937
|
// "reached" not "navigated": the frame goes to every connected surface but only the
|
|
3919
3938
|
// VISIBLE one acts on it, so a count of sockets is not a count of things that moved.
|
|
3920
|
-
console.log(color.dim(` ${r.delivered > 0 ? `
|
|
3939
|
+
console.log(color.dim(` ${r.delivered > 0 ? `navigation requested (${r.delivered} connected surface(s)); editor display is not yet verified` : 'no navigation recipient connected'}`));
|
|
3921
3940
|
// Only reach for the local browser when asked, or when nothing was
|
|
3922
3941
|
// delivered — opening a window under someone who already has the tab
|
|
3923
3942
|
// focused is worse than doing nothing.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* beehaven agents-md — put the Clearly block in THIS project's AGENTS.md (CAN2-66).
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ TWO FILES, TWO AUDIENCES. `beehaven login` already writes a GLOBAL block into
|
|
5
|
+
* ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md and ~/.gemini/GEMINI.md (agent-instructions.ts), so every
|
|
6
|
+
* agent on THIS machine knows Clearly. A project's AGENTS.md is for everyone else who opens the
|
|
7
|
+
* repo — a teammate's Codex, a cloud agent, CI — and it is committed and shared. That is why this
|
|
8
|
+
* is its own command the person (or their agent, asked first) runs, not a side effect of install.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ THE MARKERS ARE THE UPDATE MECHANISM, as on every other surface (lib/connections/agentsMd.ts):
|
|
11
|
+
* re-running replaces only what lies between our markers and leaves the rest byte for byte. The
|
|
12
|
+
* CLI and MCP blocks have their own markers, so a repo used both ways keeps both.
|
|
13
|
+
*
|
|
14
|
+
* ⚠ AGENTS.md IS THE FILE; CLAUDE.md POINTS AT IT with one `@AGENTS.md` line — one copy to keep
|
|
15
|
+
* current. An existing CLAUDE.md gains the line only if it lacks it; nothing else in it is touched.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
18
|
+
import { join, resolve } from 'path';
|
|
19
|
+
import { execFileSync } from 'child_process';
|
|
20
|
+
import { color } from '../spinner.js';
|
|
21
|
+
import { CLI_BLOCK_START, CLI_BLOCK_END, MCP_BLOCK_START, MCP_BLOCK_END, CLAUDE_MD_IMPORT, agentsMdCliBlock, agentsMdMcpBlock, } from '../agents-md-text.js';
|
|
22
|
+
/** Our block replaced between its markers, or appended after one blank line. Keeps CRLF files CRLF. */
|
|
23
|
+
export function spliceBlock(text, block, start, end) {
|
|
24
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
25
|
+
const b = block.split('\n').join(eol);
|
|
26
|
+
const i = text.indexOf(start);
|
|
27
|
+
const j = i >= 0 ? text.indexOf(end, i) : -1;
|
|
28
|
+
if (i >= 0 && j >= 0)
|
|
29
|
+
return text.slice(0, i) + b + text.slice(j + end.length);
|
|
30
|
+
if (!text.trim())
|
|
31
|
+
return b + eol;
|
|
32
|
+
return text.replace(/\s*$/, '') + eol + eol + b + eol;
|
|
33
|
+
}
|
|
34
|
+
/** Our block taken out, with the blank line that separated it. Everything else stays. */
|
|
35
|
+
export function removeBlock(text, start, end) {
|
|
36
|
+
const i = text.indexOf(start);
|
|
37
|
+
const j = i >= 0 ? text.indexOf(end, i) : -1;
|
|
38
|
+
if (i < 0 || j < 0)
|
|
39
|
+
return text;
|
|
40
|
+
const before = text.slice(0, i).replace(/(\r?\n)*$/, '');
|
|
41
|
+
const after = text.slice(j + end.length).replace(/^(\r?\n)*/, '');
|
|
42
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
43
|
+
if (!before)
|
|
44
|
+
return after;
|
|
45
|
+
return after ? before + eol + eol + after : before + eol;
|
|
46
|
+
}
|
|
47
|
+
/** The repository root when `dir` is inside a git work tree, else `dir` itself. */
|
|
48
|
+
export function projectRoot(dir) {
|
|
49
|
+
try {
|
|
50
|
+
const top = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
51
|
+
cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, timeout: 5_000,
|
|
52
|
+
}).trim();
|
|
53
|
+
return top || dir;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return dir;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Write (or remove) the block in `<root>/AGENTS.md` and make CLAUDE.md import it. */
|
|
60
|
+
export function writeProjectAgentsMd(opts) {
|
|
61
|
+
const root = projectRoot(resolve(opts.dir));
|
|
62
|
+
const [start, end] = opts.mcp ? [MCP_BLOCK_START, MCP_BLOCK_END] : [CLI_BLOCK_START, CLI_BLOCK_END];
|
|
63
|
+
const agentsPath = join(root, 'AGENTS.md');
|
|
64
|
+
const claudePath = join(root, 'CLAUDE.md');
|
|
65
|
+
const files = [];
|
|
66
|
+
const had = existsSync(agentsPath);
|
|
67
|
+
const before = had ? readFileSync(agentsPath, 'utf8') : '';
|
|
68
|
+
if (opts.remove) {
|
|
69
|
+
const next = removeBlock(before, start, end);
|
|
70
|
+
if (next !== before)
|
|
71
|
+
writeFileSync(agentsPath, next);
|
|
72
|
+
files.push({ path: agentsPath, change: next !== before ? 'removed' : 'absent' });
|
|
73
|
+
return { root, files };
|
|
74
|
+
}
|
|
75
|
+
const next = spliceBlock(before, opts.mcp ? agentsMdMcpBlock() : agentsMdCliBlock(), start, end);
|
|
76
|
+
if (next !== before)
|
|
77
|
+
writeFileSync(agentsPath, next);
|
|
78
|
+
files.push({ path: agentsPath, change: !had ? 'created' : next !== before ? 'updated' : 'current' });
|
|
79
|
+
const claude = existsSync(claudePath) ? readFileSync(claudePath, 'utf8') : null;
|
|
80
|
+
if (claude === null) {
|
|
81
|
+
writeFileSync(claudePath, CLAUDE_MD_IMPORT + '\n');
|
|
82
|
+
files.push({ path: claudePath, change: 'created' });
|
|
83
|
+
}
|
|
84
|
+
else if (!claude.split(/\r?\n/).some((l) => l.trim() === CLAUDE_MD_IMPORT)) {
|
|
85
|
+
const eol = claude.includes('\r\n') ? '\r\n' : '\n';
|
|
86
|
+
writeFileSync(claudePath, claude.replace(/\s*$/, '') + (claude.trim() ? eol + eol : '') + CLAUDE_MD_IMPORT + eol);
|
|
87
|
+
files.push({ path: claudePath, change: 'updated' });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
files.push({ path: claudePath, change: 'current' });
|
|
91
|
+
}
|
|
92
|
+
return { root, files };
|
|
93
|
+
}
|
|
94
|
+
export function cmdAgentsMd(dir, opts) {
|
|
95
|
+
if (opts.print) {
|
|
96
|
+
process.stdout.write((opts.mcp ? agentsMdMcpBlock() : agentsMdCliBlock()) + '\n');
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
let r;
|
|
100
|
+
try {
|
|
101
|
+
r = writeProjectAgentsMd({ dir: dir ?? process.cwd(), mcp: opts.mcp, remove: opts.remove });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.error(` ${color.red('✗')} ${err?.message ?? err}`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
console.log('');
|
|
108
|
+
for (const f of r.files) {
|
|
109
|
+
const mark = f.change === 'current' || f.change === 'absent' ? color.dim('·') : color.green('✓');
|
|
110
|
+
console.log(` ${mark} ${f.path} ${color.dim(f.change)}`);
|
|
111
|
+
}
|
|
112
|
+
const changed = r.files.some((f) => f.change !== 'current' && f.change !== 'absent');
|
|
113
|
+
if (changed && !opts.remove) {
|
|
114
|
+
console.log(color.dim(`\n Commit ${r.files.map((f) => f.path.slice(r.root.length + 1)).join(' and ')} so every agent that opens this project knows the workspace.`));
|
|
115
|
+
}
|
|
116
|
+
console.log('');
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
@@ -42,6 +42,8 @@ export function spawnStandaloneDaemon(values) {
|
|
|
42
42
|
const child = spawn(process.execPath, [entry], {
|
|
43
43
|
detached: true,
|
|
44
44
|
stdio: 'ignore',
|
|
45
|
+
// Windows: a detached child gets its OWN console window — closing it kills the daemon. Hide it.
|
|
46
|
+
windowsHide: true,
|
|
45
47
|
// BEEHAVEN_HEADLESS=1 is REQUIRED, not optional: daemon-entry.ts runs a
|
|
46
48
|
// parent-death watchdog (polls process.ppid, self-exits ~3s after the parent
|
|
47
49
|
// is gone) to enforce the tray-coupled lifecycle. A detached CLI-spawned
|
package/dist/commands/help.js
CHANGED
|
@@ -413,6 +413,27 @@ const PAGES = {
|
|
|
413
413
|
\`beehaven login\` to connect the CLI to it.`,
|
|
414
414
|
related: ['login', 'guide'],
|
|
415
415
|
},
|
|
416
|
+
'agents-md': {
|
|
417
|
+
synopsis: 'beehaven agents-md [dir] [--mcp] [--print] [--remove]',
|
|
418
|
+
description: `Add the Clearly block to this project's AGENTS.md, so every agent that opens the
|
|
419
|
+
repository — a teammate's Codex or Claude Code, a cloud agent, CI — knows the workspace and how
|
|
420
|
+
to reach it. Writes at the git root (or the directory given), and makes CLAUDE.md import it with
|
|
421
|
+
one \`@AGENTS.md\` line. Needs no login.
|
|
422
|
+
|
|
423
|
+
The block sits between Clearly's markers: run it again to update the block, and everything
|
|
424
|
+
else in the file stays as it was. \`beehaven login\` already teaches the agents on THIS machine
|
|
425
|
+
(their global instruction files); this is the shared, committed copy for everyone else.`,
|
|
426
|
+
flags: [
|
|
427
|
+
{ name: '--mcp', desc: 'Write the block for agents connected over MCP instead of the CLI' },
|
|
428
|
+
{ name: '--print', desc: 'Print the block and write nothing' },
|
|
429
|
+
{ name: '--remove', desc: 'Take the block out again' },
|
|
430
|
+
],
|
|
431
|
+
examples: [
|
|
432
|
+
{ cmd: 'beehaven agents-md', note: 'AGENTS.md + CLAUDE.md at the repository root' },
|
|
433
|
+
{ cmd: 'beehaven agents-md --mcp', note: 'for a team that connects over MCP' },
|
|
434
|
+
],
|
|
435
|
+
related: ['skills', 'login'],
|
|
436
|
+
},
|
|
416
437
|
handoff: {
|
|
417
438
|
synopsis: 'beehaven handoff [file.md|-] [--tasks tasks.md|json] [--csv data.csv]… [--title "…"] [--wait] [--json]',
|
|
418
439
|
description: `Give a person your work when they have no Clearly account yet. Needs no login.
|
|
@@ -43,9 +43,10 @@ import { execFileSync } from 'node:child_process';
|
|
|
43
43
|
import { createHash } from 'node:crypto';
|
|
44
44
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
45
45
|
import { homedir, tmpdir } from 'node:os';
|
|
46
|
-
import { join, sep } from 'node:path';
|
|
46
|
+
import { dirname, join, sep } from 'node:path';
|
|
47
47
|
import { fileURLToPath } from 'node:url';
|
|
48
|
-
import { usableUpdateDigest } from '../version-utils.js';
|
|
48
|
+
import { compareVersions, usableUpdateDigest } from '../version-utils.js';
|
|
49
|
+
import { npmUpdatePlan, latestNpmVersion, installedNpmVersion, installNpmVersionSync } from '../npm-update.js';
|
|
49
50
|
/**
|
|
50
51
|
* Which install this process is.
|
|
51
52
|
*
|
|
@@ -83,8 +84,9 @@ export function updateHint(kind = installKind()) {
|
|
|
83
84
|
return 'Run \x1b[1mbeehaven update\x1b[0m';
|
|
84
85
|
if (kind === 'npx')
|
|
85
86
|
return 'Run `npx @clearlysh/beehaven@latest` for the current npm release.';
|
|
87
|
+
// npm installs update themselves since CAN2-57 (the daemon hourly, `beehaven update` on demand).
|
|
86
88
|
if (kind === 'npm')
|
|
87
|
-
return 'Run `npm install -g @clearlysh/beehaven@latest`
|
|
89
|
+
return 'Run \x1b[1mbeehaven update\x1b[0m (or `npm install -g @clearlysh/beehaven@latest`).';
|
|
88
90
|
return 'This is a linked dev build — `npm run build` in beehaven_sdk/packages/cli.';
|
|
89
91
|
}
|
|
90
92
|
/**
|
|
@@ -113,9 +115,11 @@ export function runSelfUpdate(kind = installKind()) {
|
|
|
113
115
|
if (kind === 'linked') {
|
|
114
116
|
return { ok: false, message: 'This is a linked development build — refusing to overwrite it.\n cd beehaven_sdk/packages/cli && npm run build' };
|
|
115
117
|
}
|
|
116
|
-
if (kind === '
|
|
118
|
+
if (kind === 'npx') {
|
|
117
119
|
return { ok: false, message: updateHint(kind) };
|
|
118
120
|
}
|
|
121
|
+
if (kind === 'npm')
|
|
122
|
+
return runNpmSelfUpdate();
|
|
119
123
|
const feed = (process.env.BEEHAVEN_CLI_FEED || 'https://downloads.clearly.sh/cli/').replace(/\/+$/, '');
|
|
120
124
|
const installerUrl = process.env.BEEHAVEN_INSTALL_URL || 'https://www.clearly.sh/install.sh';
|
|
121
125
|
const temp = mkdtempSync(join(tmpdir(), 'beehaven-self-update-'));
|
|
@@ -154,9 +158,32 @@ export function runSelfUpdate(kind = installKind()) {
|
|
|
154
158
|
rmSync(temp, { recursive: true, force: true });
|
|
155
159
|
}
|
|
156
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* ⚠ npm installs (the only Windows install) update with npm itself — see `npm-update.ts` for why
|
|
163
|
+
* it refuses rather than guessing when this copy is not npm's global install or the prefix is not
|
|
164
|
+
* writable. The daemon restarts itself after ITS hourly update; after this manual one the running
|
|
165
|
+
* daemon keeps the old code until restarted, so the message says so.
|
|
166
|
+
*/
|
|
167
|
+
function runNpmSelfUpdate() {
|
|
168
|
+
const manual = 'Update it by hand: npm install -g @clearlysh/beehaven@latest';
|
|
169
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
170
|
+
const plan = npmUpdatePlan(pkgRoot);
|
|
171
|
+
if (!plan.ok)
|
|
172
|
+
return { ok: false, message: `Can't update this npm install here: ${plan.reason}.\n ${manual}` };
|
|
173
|
+
const latest = latestNpmVersion(plan.npm);
|
|
174
|
+
if (!latest)
|
|
175
|
+
return { ok: false, message: `Update failed: could not read the latest version from npm.\n ${manual}` };
|
|
176
|
+
const current = installedNpmVersion(plan.globalRoot) ?? '0.0.0';
|
|
177
|
+
if (compareVersions(latest, current) <= 0)
|
|
178
|
+
return { ok: true, message: `Already up to date (${current}).` };
|
|
179
|
+
const r = installNpmVersionSync(plan, latest);
|
|
180
|
+
return r.ok
|
|
181
|
+
? { ok: true, message: `${r.message} A daemon that is already running keeps the old version until: beehaven restart` }
|
|
182
|
+
: { ok: false, message: `${r.message}\n ${manual}` };
|
|
183
|
+
}
|
|
157
184
|
/** True when this install can update itself at all — what the notifier uses to pick its wording. */
|
|
158
185
|
export function canSelfUpdate(kind = installKind()) {
|
|
159
|
-
return kind === 'user';
|
|
186
|
+
return kind === 'user' || kind === 'npm';
|
|
160
187
|
}
|
|
161
188
|
/**
|
|
162
189
|
* Is a bundled CLI present that the shell can NEVER reach, whatever its version?
|
|
@@ -26,7 +26,10 @@ import { resolveActiveAgent } from '../agent-state.js';
|
|
|
26
26
|
import { isAgentRuntime } from '../agent-scope.js';
|
|
27
27
|
import { getTarget } from '../cli-state.js';
|
|
28
28
|
import { attachShellCwd, absorbShellCwd, shellTargetKey } from '../shell-session.js';
|
|
29
|
-
|
|
29
|
+
/** Workspace-agent jobs wait for the server's ceiling (900s) plus the report's tail. */
|
|
30
|
+
export const AGENT_TURN_TIMEOUT_MS = 930_000;
|
|
31
|
+
const AGENT_TURN_ACTIONS = new Set(['agent-prompt', 'agent-turn']);
|
|
32
|
+
export function getTimeout(values, action) {
|
|
30
33
|
// Precedence: explicit --timeout flag → BEEHAVEN_RPC_TIMEOUT_MS env → default.
|
|
31
34
|
// The default is generous because real jobs (sticker-sheet gen, renders,
|
|
32
35
|
// long agent turns) routinely run well past the old 30s and shouldn't need a
|
|
@@ -37,7 +40,8 @@ export function getTimeout(values) {
|
|
|
37
40
|
const env = process.env.BEEHAVEN_RPC_TIMEOUT_MS;
|
|
38
41
|
if (env && Number.isFinite(Number(env)) && Number(env) > 0)
|
|
39
42
|
return parseInt(env, 10);
|
|
40
|
-
|
|
43
|
+
// An agent job may run to the server's 900s ceiling (WDP3-62); waiting less abandons it.
|
|
44
|
+
return action && AGENT_TURN_ACTIONS.has(action) ? AGENT_TURN_TIMEOUT_MS : 180_000;
|
|
41
45
|
}
|
|
42
46
|
/**
|
|
43
47
|
* True when `msg` says `action` isn't available on the target DO.
|
|
@@ -618,11 +622,24 @@ export async function cmdActions(values, positional) {
|
|
|
618
622
|
grouped.set(cat, []);
|
|
619
623
|
grouped.get(cat).push(action);
|
|
620
624
|
}
|
|
625
|
+
if (!values.verbose && !values.category && !values.search) {
|
|
626
|
+
for (const [category, entries] of grouped) {
|
|
627
|
+
console.log(` ${category.padEnd(18)} ${String(entries.length).padStart(3)} actions · ${entries.slice(0, 3).map(a => a.name).join(', ')}${entries.length > 3 ? ', …' : ''}`);
|
|
628
|
+
}
|
|
629
|
+
console.log('');
|
|
630
|
+
console.log(' Find an action: beehaven actions --search <term>');
|
|
631
|
+
console.log(' List a category: beehaven actions --category <name>');
|
|
632
|
+
console.log(' Exact schema: beehaven explain <action>');
|
|
633
|
+
console.log(' Complete inventory: beehaven actions --verbose or --format full --json');
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
621
636
|
for (const [category, catActions] of grouped) {
|
|
622
637
|
const catLabel = category.charAt(0).toUpperCase() + category.slice(1);
|
|
623
638
|
console.log(` \x1b[36m${catLabel}\x1b[0m (${catActions.length})`);
|
|
624
639
|
for (const a of catActions) {
|
|
625
|
-
const
|
|
640
|
+
const description = String(a.description || '');
|
|
641
|
+
const display = values.verbose || description.length <= 160 ? description : `${description.slice(0, 157)}…`;
|
|
642
|
+
const desc = display ? ` \x1b[90m${display}\x1b[0m` : '';
|
|
626
643
|
const aliasLabel = Array.isArray(a.aliases) && a.aliases.length > 0
|
|
627
644
|
? ` \x1b[2m(also: ${a.aliases.join(', ')})\x1b[0m`
|
|
628
645
|
: '';
|
|
@@ -1088,7 +1105,7 @@ export async function cmdCall(action, rawInput, values) {
|
|
|
1088
1105
|
console.error(`\x1b[2m→ ${target} ${action}${who}\x1b[0m`);
|
|
1089
1106
|
}
|
|
1090
1107
|
try {
|
|
1091
|
-
const raw = await rpcOnce(resolved.action, resolved.input, getTimeout(values));
|
|
1108
|
+
const raw = await rpcOnce(resolved.action, resolved.input, getTimeout(values, action));
|
|
1092
1109
|
// Unwrap proxy envelope: admin:*-rpc wraps results in { type: 'ack', result: ... }
|
|
1093
1110
|
const result = raw?.type === 'ack' && raw?.result !== undefined
|
|
1094
1111
|
? raw.result
|
|
@@ -3090,7 +3107,7 @@ export async function cmdAgent(prompt, values) {
|
|
|
3090
3107
|
: {}),
|
|
3091
3108
|
},
|
|
3092
3109
|
// Omni runs tools; the CLI default is far too short for a real turn.
|
|
3093
|
-
getTimeout(values
|
|
3110
|
+
getTimeout(values, 'agent-turn'));
|
|
3094
3111
|
/**
|
|
3095
3112
|
* Print the REPLY, not the envelope. The whole response is still available
|
|
3096
3113
|
* with `--json` — but a verb whose entire purpose is "ask a question and read
|
package/dist/daemon-client.js
CHANGED
|
@@ -15,17 +15,16 @@
|
|
|
15
15
|
* }
|
|
16
16
|
*/
|
|
17
17
|
import net from 'net';
|
|
18
|
-
import { existsSync } from 'fs';
|
|
19
18
|
import { resolveClientAuthToken } from './ipc-auth.js';
|
|
20
19
|
import { activeAgentWrap } from './agent-attribution.js';
|
|
21
|
-
|
|
20
|
+
import { ipcSocketPath, socketMayExist } from './ipc-path.js';
|
|
22
21
|
const TIMEOUT_MS = 60_000;
|
|
23
22
|
const AUTH_TIMEOUT_MS = 3_000;
|
|
24
23
|
/** One attempt. Resolves, never rejects. */
|
|
25
24
|
function probeOnce(deadlineMs) {
|
|
26
25
|
return new Promise((resolve) => {
|
|
27
26
|
let connected = false;
|
|
28
|
-
const sock = net.createConnection(
|
|
27
|
+
const sock = net.createConnection(ipcSocketPath());
|
|
29
28
|
const timer = setTimeout(() => {
|
|
30
29
|
sock.destroy();
|
|
31
30
|
// ⚠ CONNECTED-BUT-SILENT IS NOT "DOWN". The kernel accepted us, so a listener exists.
|
|
@@ -65,7 +64,7 @@ const PROBE_BACKOFF_MS = 150;
|
|
|
65
64
|
* that "down" is the original defect.
|
|
66
65
|
*/
|
|
67
66
|
export async function probeDaemon() {
|
|
68
|
-
if (!
|
|
67
|
+
if (!socketMayExist(ipcSocketPath()))
|
|
69
68
|
return { state: 'down', attempts: 0 };
|
|
70
69
|
let sawListener = false;
|
|
71
70
|
let last = { state: 'down', attempts: 0 };
|
|
@@ -74,6 +73,11 @@ export async function probeDaemon() {
|
|
|
74
73
|
last = { ...r, attempts: i };
|
|
75
74
|
if (r.state === 'up')
|
|
76
75
|
return last;
|
|
76
|
+
// Nothing at the address (no socket file, or no such Windows pipe): permanent, like the
|
|
77
|
+
// missing-file check above — a named pipe cannot be checked before connecting, so this is
|
|
78
|
+
// where "nothing is running" is learned on Windows. Retrying would only add ~300 ms.
|
|
79
|
+
if (r.code === 'ENOENT' && !sawListener)
|
|
80
|
+
return last;
|
|
77
81
|
if (r.state === 'silent')
|
|
78
82
|
sawListener = true;
|
|
79
83
|
if (i < PROBE_ATTEMPTS)
|
|
@@ -165,7 +169,7 @@ function handshake(socket) {
|
|
|
165
169
|
* ⚠ `start --headless` NOW WAITS for the socket (see `daemon-cmd.ts`), so a script
|
|
166
170
|
* that uses it should no longer reach this at all.
|
|
167
171
|
*/
|
|
168
|
-
(
|
|
172
|
+
(socketMayExist(ipcSocketPath())
|
|
169
173
|
? 'The daemon is still starting — its socket is up but it has not minted an IPC token yet. ' +
|
|
170
174
|
'Wait a second and retry. (Do NOT restart: `stop` scrubs the token, which re-enters this window.)'
|
|
171
175
|
: 'No IPC auth token and no daemon socket — nothing is running. ' +
|
|
@@ -192,7 +196,7 @@ function handshake(socket) {
|
|
|
192
196
|
}
|
|
193
197
|
/** Open a socket, complete auth handshake, run a single request, close. */
|
|
194
198
|
async function withSocket(fn) {
|
|
195
|
-
const socket = net.createConnection(
|
|
199
|
+
const socket = net.createConnection(ipcSocketPath());
|
|
196
200
|
await new Promise((resolve, reject) => {
|
|
197
201
|
socket.once('connect', () => resolve());
|
|
198
202
|
socket.once('error', reject);
|
package/dist/daemon-entry.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* startup errors (those usually need user intervention — expired token,
|
|
24
24
|
* missing config, etc).
|
|
25
25
|
*/
|
|
26
|
+
import './windows-hide.js'; // FIRST: no console windows flashing on Windows (CAN2-56)
|
|
26
27
|
import { startDaemon } from './daemon.js';
|
|
27
28
|
// ── Parent-watcher (tray-coupled enforcement) ──
|
|
28
29
|
// The daemon is documented as tray-coupled: it must not outlive the
|