@cmgmyr/hive 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/claude-plugin/.claude-plugin/plugin.json +7 -0
  4. package/claude-plugin/README.md +47 -0
  5. package/claude-plugin/hooks/hooks.json +14 -0
  6. package/claude-plugin/kickoff.mjs +41 -0
  7. package/claude-plugin/skills/cleanup/SKILL.md +60 -0
  8. package/dist/abi.js +220 -0
  9. package/dist/abiProbe.js +9 -0
  10. package/dist/backgroundTasks.js +51 -0
  11. package/dist/backup.js +378 -0
  12. package/dist/brief.js +113 -0
  13. package/dist/build-info.json +1 -0
  14. package/dist/claudeDir.js +5 -0
  15. package/dist/cli.js +2901 -0
  16. package/dist/codexHome.js +297 -0
  17. package/dist/config.js +72 -0
  18. package/dist/context.js +256 -0
  19. package/dist/contextCheckpoint.js +55 -0
  20. package/dist/dashboard.js +1158 -0
  21. package/dist/dataDir.js +89 -0
  22. package/dist/db.js +421 -0
  23. package/dist/dispatcher.js +150 -0
  24. package/dist/firstPrompt.js +6 -0
  25. package/dist/harnesses.js +173 -0
  26. package/dist/help.js +353 -0
  27. package/dist/hook.js +197 -0
  28. package/dist/hooks.js +50 -0
  29. package/dist/index.js +34 -0
  30. package/dist/kickoff.js +191 -0
  31. package/dist/leadMessage.js +98 -0
  32. package/dist/mcpConfig.js +121 -0
  33. package/dist/processes.js +161 -0
  34. package/dist/profiles.js +278 -0
  35. package/dist/projectYml.js +254 -0
  36. package/dist/prompts.js +18 -0
  37. package/dist/ptys.js +146 -0
  38. package/dist/result.js +53 -0
  39. package/dist/scheduler.js +1622 -0
  40. package/dist/sessionProbe.js +131 -0
  41. package/dist/slug.js +29 -0
  42. package/dist/spawn.js +370 -0
  43. package/dist/stateProvenance.js +97 -0
  44. package/dist/statusline.js +82 -0
  45. package/dist/strictInput.js +52 -0
  46. package/dist/teardown.js +89 -0
  47. package/dist/tmux.js +1295 -0
  48. package/dist/tools/agents.js +1246 -0
  49. package/dist/tools/kv.js +89 -0
  50. package/dist/tools/leases.js +87 -0
  51. package/dist/tools/meta.js +183 -0
  52. package/dist/tools/pads.js +279 -0
  53. package/dist/tools/params.js +14 -0
  54. package/dist/tools/todos.js +382 -0
  55. package/dist/tools/wakes.js +466 -0
  56. package/dist/transcript.js +123 -0
  57. package/dist/version.js +59 -0
  58. package/package.json +62 -0
  59. package/profiles/orchestration/posture.md +27 -0
  60. package/profiles/orchestration/runbook.md +72 -0
  61. package/profiles/orchestration/worker.md +38 -0
  62. package/profiles/simple/posture.md +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Gmyr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ <p align="center"><img src="https://raw.githubusercontent.com/cmgmyr/hive/main/docs/assets/logo-card.svg" alt="hive" height="112"></p>
2
+ <p align="center"><a href="https://github.com/cmgmyr/hive/actions/workflows/ci.yml"><img src="https://github.com/cmgmyr/hive/actions/workflows/ci.yml/badge.svg" alt="CI"></a></p>
3
+
4
+ Shared, persistent memory and a visible crew of tmux workers for Claude Code and Codex sessions on one project.
5
+
6
+ - Visible workers: each one is a real tmux pane you can read and type into.
7
+ - Exact state: workers report it through their own CLI's hooks, so nothing polls.
8
+ - One local SQLite store: no daemon, and nothing leaves your machine.
9
+
10
+ ![hive: a lead plans, spawns workers, and is woken when they finish](https://raw.githubusercontent.com/cmgmyr/hive/main/docs/assets/demo.svg)
11
+
12
+ ## Install
13
+
14
+ Requirements: macOS, Node `^22.14.0 || >=23.6.0`, [Claude Code](https://claude.com/claude-code), and tmux for the agent tools. codex is optional, only needed if a project opts a worker into it; see [docs/install.md](docs/install.md#codex-workers).
15
+
16
+ ```bash
17
+ git clone https://github.com/cmgmyr/hive.git hive && cd hive
18
+ npm install
19
+ npm run build
20
+ npm link # puts the hive command on your PATH
21
+ hive setup # pins that command to one interpreter
22
+ brew install tmux
23
+ claude mcp add --scope user hive -- "$(command -v node)" "$(pwd)/dist/index.js"
24
+ ln -s "$(pwd)/claude-plugin" ~/.claude/skills/hive # optional: session-start kickoff
25
+ hive doctor # verify: node, ABI, tmux, claude, database, hooks all green
26
+ ```
27
+
28
+ Put `~/.local/bin` on your PATH below your version manager's block. See [Node version and the interpreter pin](docs/install.md#node-version-and-the-interpreter-pin) for why the order matters.
29
+
30
+ ## First run
31
+
32
+ Run `cd ~/Code/your-project && hive`. It is shorthand for `hive lead`, and it opens a `lead` window running Claude in this project's tmux session, with the lead session named after the project so your other Claude Code sessions can address it by that name; ask it to triage, and it reads the standing process and proposes work. Spawn workers with `agent_spawn`, and watch or take over any of them with `tmux -CC attach -t hive-main` (or plain `tmux attach`).
33
+
34
+ ## How it works
35
+
36
+ Each Claude Code session runs its own `hive` MCP server over stdio, and every instance reads and writes one SQLite database (WAL mode) at `~/.hive/hive.db`, so every session sees the same state. There is no daemon and nothing leaves your machine. State is scoped to a project (a directory), resolved from the working directory; a lead spawns workers into tmux panes locked to that project.
37
+
38
+ ## Why not subagents?
39
+
40
+ | | Subagents | hive workers |
41
+ |---|---|---|
42
+ | Visibility | report at the end | live terminal you read and type into |
43
+ | Persistence | vanish with the conversation | pads and todos outlive every session |
44
+ | Lifetime | die with the parent | keep running when the lead detaches |
45
+ | Scope | one session | several sessions, terminals, humans |
46
+
47
+ Hive workers can still use subagents. See [Why not subagents?](docs/concepts.md#why-not-subagents) for the longer answer.
48
+
49
+ ## Status
50
+
51
+ hive is a personal daily-driver tool, released low-key. It is single-user by design and dogfooded daily by its author on macOS. The test suite also runs on Linux in CI, but nobody drives hive there yet. Issues are welcome; for bigger changes, open a discussion first. See [CONTRIBUTING.md](CONTRIBUTING.md). MIT licensed.
52
+
53
+ ## Docs
54
+
55
+ | Page | What's there |
56
+ |---|---|
57
+ | [Architecture](docs/architecture.md) | Seven diagrams: process topology, module layering, spawn sequence, wake lifecycle, worker state, project scoping, store and server identity |
58
+ | [Patterns](docs/patterns.md) | Standing trades, refused approaches, evidence standards, and guard shapes distilled from the project's own decisions and dead-ends |
59
+ | [Concepts](docs/concepts.md) | Vocabulary, why not subagents, identity, the workflow, project scope, the shared store |
60
+ | [Daily driver](docs/daily-driver.md) | A day with hive, starting a session, watching workers, wake-ups |
61
+ | [Commands](docs/commands.md) | Every `hive` subcommand and what it does |
62
+ | [Configuration](docs/configuration.md) | The `HIVE_*` environment variables |
63
+ | [Profiles](docs/profiles.md) | Standing instructions across projects, the session-start plugin |
64
+ | [Projects](docs/projects.md) | `hive init`, `hive.yml`, automatic backups, pads and todos from the shell |
65
+ | [Dashboard](docs/dashboard.md) | The generated dashboard: enabling it, where it lives, what it shows |
66
+ | [Install details](docs/install.md) | The interpreter pin, iTerm settings, the status line, MCP scope, codex workers, updating, uninstalling |
67
+ | [Troubleshooting](docs/troubleshooting.md) | Common errors and their fixes |
68
+ | [Tools](docs/tools.md) | The 45 MCP tools: what each does and when to use it |
69
+ | [tmux settings](docs/tmux.md) | Attach modes, pane options, and what to put in `~/.tmux.conf` |
70
+ | [Development](docs/development.md) | Building and testing hive itself |
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
+ "name": "hive",
4
+ "displayName": "hive",
5
+ "description": "Injects hive's live project state at session start when you open a lead session in a hive project.",
6
+ "version": "0.1.0"
7
+ }
@@ -0,0 +1,47 @@
1
+ # hive plugin
2
+
3
+ Registers one SessionStart hook. When you open a session in a hive project's
4
+ root, on a lead branch, with a profile hive can resolve, it injects the
5
+ project's live state (board pad, in-flight and dispatchable todos, running
6
+ workers, pending wake-ups) and asks the lead to run triage. Everywhere else it
7
+ prints nothing.
8
+
9
+ The gates live in `hive kickoff`, not here, so they upgrade with the binary
10
+ instead of going stale in a copied script. Run `hive kickoff --explain` in any
11
+ directory to see what it decided and why.
12
+
13
+ ## Install
14
+
15
+ ```
16
+ ln -s <checkout>/claude-plugin ~/.claude/skills/hive
17
+ ```
18
+
19
+ That is the whole install. A folder under a skills directory holding
20
+ `.claude-plugin/plugin.json` loads as a plugin on the next session, discovered
21
+ in place rather than copied, so `git pull && npm run build` upgrades the hook
22
+ along with the rest of hive. `rm ~/.claude/skills/hive` uninstalls it.
23
+
24
+ The hook lives here, and so does a skill (`skills/cleanup`, `/hive:cleanup`).
25
+ hive's posture and worker briefs still travel as command-line flags from
26
+ `hive lead` and `agent_spawn`, because a plugin has no way to ship always-on
27
+ instructions: plugin `CLAUDE.md` is not loaded as context, and the one
28
+ always-on mechanism a plugin has, a `force-for-plugin` output style, would
29
+ override whatever output style you have selected - hive must never do that. A
30
+ skill is not always-on, which is exactly why it is fine here where an output
31
+ style is not: it costs nothing on a session that never matches it, and it
32
+ overrides nothing the user chose. It only acts when its own description
33
+ matches the moment, the same as any other skill.
34
+
35
+ ## Why kickoff.mjs exists
36
+
37
+ `${CLAUDE_PLUGIN_ROOT}` is the path the plugin was found at. For the install
38
+ above that is the symlink, `~/.claude/skills/hive`, not the checkout. node
39
+ normalizes `..` lexically before touching the filesystem, so a hook command of
40
+ `node "${CLAUDE_PLUGIN_ROOT}"/../dist/kickoff.js` collapses to
41
+ `~/.claude/skills/dist/kickoff.js` and fails with MODULE_NOT_FOUND, even
42
+ though the kernel would have resolved that same path fine.
43
+
44
+ `kickoff.mjs` sidesteps it. The hook command has no `..` in it, so the symlink
45
+ resolves normally; node then realpaths the main entry, which puts the shim's
46
+ own `../dist/kickoff.js` import inside the real checkout. No absolute machine
47
+ path is ever written into a committed file.
@@ -0,0 +1,14 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/kickoff.mjs\""
9
+ }
10
+ ]
11
+ }
12
+ ]
13
+ }
14
+ }
@@ -0,0 +1,41 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ async function reexecUnderPinnedInterpreter() {
7
+
8
+ if (process.env.HIVE_KICKOFF_REEXEC) return;
9
+
10
+ if (process.env.HIVE_AGENT_ID && process.env.HIVE_LEAD !== "1") return;
11
+ if (!existsSync(join(process.cwd(), "hive.yml"))) return;
12
+
13
+ const { checkAbi } = await import("../dist/abi.js");
14
+ if (checkAbi().ok) return;
15
+
16
+ const { dispatcherPath, readDispatcher } = await import("../dist/dispatcher.js");
17
+ const dispatcher = readDispatcher(dispatcherPath());
18
+
19
+ const node = dispatcher?.node ?? null;
20
+
21
+ if (!node) return;
22
+
23
+ if (node === process.execPath || !existsSync(node)) return;
24
+
25
+ const result = spawnSync(node, [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
26
+ stdio: "inherit",
27
+ env: { ...process.env, HIVE_KICKOFF_REEXEC: "1" },
28
+ });
29
+
30
+ if (result.error) return;
31
+ process.exit(result.status ?? 1);
32
+ }
33
+
34
+ try {
35
+ await reexecUnderPinnedInterpreter();
36
+ } catch {
37
+
38
+ }
39
+
40
+ const { runKickoff } = await import("../dist/kickoff.js");
41
+ await runKickoff(process.argv.slice(2));
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: cleanup
3
+ description: Trims a hive project's board pad, closes finished todos on evidence, archives pads that describe finished work, and cancels stale wake-ups. Fires proactively, not only when asked - use it when a lane or wave has just finished, a worker was just closed, a merge just landed, the session is wrapping up, or the board pad describes state that is no longer true, as well as when the user says /hive:cleanup, "clean up the board", "tidy the store", "close this out", or "the board is stale".
4
+ ---
5
+
6
+ # hive cleanup
7
+
8
+ ## Read everything first
9
+
10
+ You cannot tell live state from dead state off a partial view. Before changing anything, read the board pad in full, `todo_list(status="open")`, `pad_list`, `agent_list`, and `wake_list`. A trim done off a truncated board deletes state nobody read.
11
+
12
+ ## The test for the board
13
+
14
+ One question, asked of every block: is this true right now? Not "was it true," not "is it interesting," not "did it take effort to learn." A board is live state. A lane that merged, a worker that is gone, a hypothesis that was killed, a baseline that was superseded, a question that got answered - all of it becomes history the moment it stops being true, however recently it was written.
15
+
16
+ ## Never delete evidence - route it
17
+
18
+ This is the rule that keeps a cleanup from being a loss. Before removing a block, decide where it belongs and put it there first:
19
+
20
+ - a number someone will reuse (a baseline, a measurement, a count) -> the todo it belongs to, as a comment, or the project's own standing-lessons record if it keeps one
21
+ - why something was decided -> a comment on that todo
22
+ - a standing lesson that outlives this work -> wherever this project keeps standing lessons, if it keeps any
23
+ - what a lane shipped -> its todo's close-out comment and the commit message
24
+ - live state that has simply stopped being live -> delete it, that is the job
25
+
26
+ The failure this guards against is specific: trimming a board and destroying the only record of a measurement, because it was written on the board and nowhere else.
27
+
28
+ ## Archive the old board before you rewrite it
29
+
30
+ `pad_archive` the board, then write a fresh one carrying forward only what is still live. The old picture stays readable with `pad_list(include_archived=true)`. This is the shipped idiom - a hive project's own starter runbook says it under board discipline - and it is what makes a bad trim recoverable instead of permanent.
31
+
32
+ ## Todos: close what is done, and only on evidence
33
+
34
+ A merge sha, a shipped file, a recorded decision - not a worker's summary and not your own memory of the session. If you cannot name the evidence, leave it open and say why.
35
+
36
+ Write the close-out comment before completing, never after and never not at all: what landed, what was deliberately not built, and what a future reader would otherwise re-derive. A todo closed with no close-out has thrown away the only durable record of the work.
37
+
38
+ `todo_complete` returns `newly_unblocked`. Say what the closure freed - that is the queue moving, and it is invisible if you do not report it.
39
+
40
+ A todo whose body has been superseded by its own comments is not done, it is misleading. Say so on the todo rather than closing it.
41
+
42
+ ## Pads: a plan pad for a lane that shipped is history
43
+
44
+ Archive it. A pad still describing in-flight state for work that finished is worse than no pad, because the next session reads it as current. Archiving is reversible and deleting is not, so archive by default and delete only what was never anything but scratch.
45
+
46
+ ## Wakes: cancel what is finished, keep what is deliberate
47
+
48
+ A watch armed for a crew that is gone is noise. A dated reminder someone set on purpose is not - cancelling one silently loses a commitment nobody will notice missing until the date passes. If you cannot tell which it is, leave it and say so.
49
+
50
+ ## Residue you report and do not remove
51
+
52
+ Worktrees, branches, running agents. Removing a worktree can destroy uncommitted work, and a running agent may be mid-turn. Name what you found and let the human or the lead decide.
53
+
54
+ ## Finish by saying what you left, not just what you changed
55
+
56
+ A cleanup that reports only its edits cannot be audited, and the interesting part is usually what was deliberately not touched. Leave the board in the shape the next session needs: where the code is now, whatever verification number this project quotes, what is genuinely in flight (often nothing), and what is waiting on a human.
57
+
58
+ ## Where to route evidence in this project
59
+
60
+ The board pad is universal - `hive init` seeds one in every hive project, and the shipped runbook template defines board discipline, so route freely to it. A pad for standing lessons is not universal; it is one team's convention, under whatever name that team gave it. Use one only if this project already has one. If it does not, put the evidence in a todo comment rather than inventing a pad the project never agreed to.
package/dist/abi.js ADDED
@@ -0,0 +1,220 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { cliPath, dispatcherPath, readDispatcher } from "./dispatcher.js";
5
+ const requireFromHere = createRequire(import.meta.url);
6
+ export function classifyAddonLoadError(error) {
7
+ return /NODE_MODULE_VERSION \d+|Module did not self-register/i.test(error) ? "mismatch" : "load";
8
+ }
9
+ export function requiredNodeApi() {
10
+ try {
11
+ const root = dirname(requireFromHere.resolve("better-sqlite3/package.json"));
12
+ const found = /NAPI_VERSION=(\d+)/.exec(readFileSync(join(root, "binding.gyp"), "utf8"));
13
+ return found ? Number(found[1]) : null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ const NODE_API_STARTS = {
20
+ 10: ["22.14.0", "23.6.0"],
21
+ };
22
+ function describeNodeApi(napi) {
23
+ return napi === null ? "none reported" : String(napi);
24
+ }
25
+ export function nodeRangeForNodeApi(napi) {
26
+ const starts = NODE_API_STARTS[napi];
27
+ if (!starts)
28
+ return null;
29
+ return starts.map((v, i) => (i === starts.length - 1 ? `>=${v}` : `^${v}`)).join(" || ");
30
+ }
31
+ function isLinuxMusl() {
32
+ if (process.platform !== "linux")
33
+ return false;
34
+ const report = process.report?.getReport();
35
+ return !report?.header?.glibcVersionRuntime;
36
+ }
37
+ function addonPath() {
38
+ try {
39
+ const root = dirname(requireFromHere.resolve("better-sqlite3/package.json"));
40
+ if (["linux", "darwin", "win32"].includes(process.platform) && ["x64", "arm64"].includes(process.arch)) {
41
+ const target = `${isLinuxMusl() ? "linuxmusl" : process.platform}-${process.arch}`;
42
+ const prebuild = join(root, "prebuilds", `${target}.node`);
43
+ if (existsSync(prebuild))
44
+ return prebuild;
45
+ }
46
+ for (const build of ["Debug", "Release"]) {
47
+ const file = join(root, "build", build, "better_sqlite3.node");
48
+ if (existsSync(file))
49
+ return file;
50
+ }
51
+ return null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export function checkAbi() {
58
+ const running = Number(process.versions.modules);
59
+ const napi = Number(process.versions.napi);
60
+ const nodeApi = Number.isFinite(napi) ? napi : null;
61
+ const nodeApiRequired = requiredNodeApi();
62
+ const addon = addonPath();
63
+ if (!addon) {
64
+ return {
65
+ addon: null,
66
+ running,
67
+ nodeApi,
68
+ nodeApiRequired,
69
+ builtFor: null,
70
+ failure: "missing",
71
+ ok: false,
72
+ error: "better-sqlite3's native addon is missing; it has not been built here",
73
+ };
74
+ }
75
+ if (nodeApiRequired !== null && (nodeApi === null || nodeApi < nodeApiRequired)) {
76
+ return {
77
+ addon,
78
+ running,
79
+ nodeApi,
80
+ nodeApiRequired,
81
+ builtFor: null,
82
+ failure: "napi",
83
+ ok: false,
84
+ error: `better-sqlite3's addon is built against Node-API ${nodeApiRequired}; this Node provides Node-API ${describeNodeApi(nodeApi)}`,
85
+ };
86
+ }
87
+ try {
88
+ requireFromHere(addon);
89
+ return { addon, running, nodeApi, nodeApiRequired, builtFor: null, failure: null, ok: true, error: null };
90
+ }
91
+ catch (e) {
92
+ const error = e instanceof Error ? e.message : String(e);
93
+ const found = [...error.matchAll(/NODE_MODULE_VERSION (\d+)/g)].map((m) => Number(m[1]));
94
+ return {
95
+ addon,
96
+ running,
97
+ nodeApi,
98
+ nodeApiRequired,
99
+ builtFor: found[0] ?? null,
100
+ failure: classifyAddonLoadError(error),
101
+ ok: false,
102
+ error,
103
+ };
104
+ }
105
+ }
106
+ export function describeInterpreter() {
107
+ return `${process.version} (NODE_MODULE_VERSION ${process.versions.modules}) at ${process.execPath}`;
108
+ }
109
+ export function describeAbi(status) {
110
+ if (status.ok) {
111
+ return status.nodeApiRequired === null
112
+ ? `addon loaded under this interpreter (NODE_MODULE_VERSION ${status.running})`
113
+ : `addon loaded; built against Node-API ${status.nodeApiRequired}, this interpreter provides Node-API ${describeNodeApi(status.nodeApi)}`;
114
+ }
115
+ if (status.failure === "napi") {
116
+ const need = status.nodeApiRequired;
117
+ const range = need === null ? null : nodeRangeForNodeApi(need);
118
+ return (`addon is built against Node-API ${need}${range ? ` (needs Node ${range})` : ""},` +
119
+ ` this interpreter provides Node-API ${describeNodeApi(status.nodeApi)}`);
120
+ }
121
+ if (status.failure === "mismatch" && status.builtFor === null) {
122
+ return `addon did not register under this interpreter (NODE_MODULE_VERSION ${status.running}); its built-for version is not reported`;
123
+ }
124
+ if (status.builtFor === null)
125
+ return status.error ?? "addon did not load";
126
+ return `addon built for NODE_MODULE_VERSION ${status.builtFor}, this interpreter needs ${status.running}`;
127
+ }
128
+ export function abiFixLines(status, pinned) {
129
+ if (status.failure === "napi") {
130
+ const need = status.nodeApiRequired;
131
+ const range = need === null ? null : nodeRangeForNodeApi(need);
132
+ const good = pinned ? `"${pinned}"` : `<a Node matching ${range ?? "that Node-API level"}>`;
133
+ return [
134
+ `better-sqlite3's addon is built against Node-API ${need}${range ? `, which only Node ${range} provides` : ""}.`,
135
+ `This interpreter provides Node-API ${describeNodeApi(status.nodeApi)}, so the addon cannot load here.`,
136
+ "Loading it anyway does not raise an error hive could report - the process dies",
137
+ "inside dlopen - so hive refuses before the load rather than after.",
138
+ "",
139
+ "Rebuilding does NOT help: better-sqlite3's own binding.gyp requests the same",
140
+ "Node-API level, so a build from source asks for exactly the same interpreter.",
141
+ "",
142
+ `Run hive under a Node matching ${range ?? "that Node-API level"}, and re-pin the \`hive\` command`,
143
+ "to it (setup pins whatever Node runs it, so it has to be that one):",
144
+ ` ${good} "${cliPath()}" setup`,
145
+ ];
146
+ }
147
+ if (status.failure === "missing") {
148
+ return [
149
+ "Reinstall the package: rm -rf node_modules/better-sqlite3 && npm install",
150
+ "",
151
+ "better-sqlite3 13 ships the addon prebuilt, so the file comes out of the",
152
+ "tarball rather than a build. A plain `npm install` will NOT bring it back:",
153
+ "with the package already unpacked at the locked version, npm reports `up to",
154
+ "date` without ever looking at the addon. Removing the directory first is what",
155
+ "makes npm fetch it again. `npm ci` does the same for the whole tree.",
156
+ "hive's own `npm run build` only compiles TypeScript and is not part of this.",
157
+ "",
158
+ "If it is still missing after that, this platform/arch has no prebuild in the",
159
+ "tarball, and the source build npm attempts during install did not succeed.",
160
+ "Run it by hand inside the package, where its output is readable:",
161
+ " cd node_modules/better-sqlite3 && npm run build-release (needs node-gyp)",
162
+ ];
163
+ }
164
+ if (status.failure !== "mismatch") {
165
+ return ["Build it: npm install && npm run build"];
166
+ }
167
+ const good = pinned ? `"${pinned}"` : "<the Node that built it>";
168
+ return [
169
+ "better-sqlite3's addon is ABI-locked to the Node that compiled it, and a Node",
170
+ "version manager resolves `node` from the working directory, so a `cd` is enough",
171
+ "to swap it.",
172
+ "",
173
+ "Run hive under the Node it was built for, and re-pin the `hive` command to it",
174
+ "(setup pins whatever Node runs it, so it has to be that one):",
175
+ ` ${good} "${cliPath()}" setup`,
176
+ "",
177
+ "Or rebuild the addon for the Node you are on, and re-pin to that:",
178
+ " npm install && npm run build && hive setup",
179
+ ];
180
+ }
181
+ function workingInterpreterFromDispatcher() {
182
+ const node = readDispatcher(dispatcherPath())?.node;
183
+ return node && node !== process.execPath && existsSync(node) ? node : null;
184
+ }
185
+ function prunedPinLines() {
186
+ const node = readDispatcher(dispatcherPath())?.node;
187
+ if (!node || existsSync(node))
188
+ return [];
189
+ return [
190
+ "",
191
+ `The \`hive\` dispatcher pins ${node}, which is not on disk. A version manager can`,
192
+ "remove one. That is also the interpreter hive's SessionStart hook re-execs into when a",
193
+ "directory's own `node` cannot load the addon, so with it gone nothing caught this.",
194
+ ];
195
+ }
196
+ export function guardAbi() {
197
+ const status = checkAbi();
198
+ if (status.ok)
199
+ return;
200
+ const doctor = process.argv[2] === "doctor";
201
+ const headline = status.failure === "mismatch"
202
+ ? "hive: cannot run under this Node."
203
+ : status.failure === "napi"
204
+ ? "hive: this Node is too old for better-sqlite3's native addon."
205
+ : status.failure === "missing"
206
+ ? "hive: better-sqlite3's native addon is not built here."
207
+ : "hive: cannot load better-sqlite3's native addon.";
208
+ const lines = [
209
+ ...(doctor ? ["hive doctor", ""] : [headline, ""]),
210
+ ` FAIL node: ${describeInterpreter()}`,
211
+ ` FAIL better-sqlite3: ${describeAbi(status)}`,
212
+ ...(status.addon ? [` ${status.addon}`] : []),
213
+ ...prunedPinLines(),
214
+ "",
215
+ ...abiFixLines(status, workingInterpreterFromDispatcher()),
216
+ ...(doctor ? ["", "1 problem(s) found, 0 warning(s)."] : []),
217
+ ];
218
+ (doctor ? console.log : console.error)(lines.join("\n"));
219
+ process.exit(1);
220
+ }
@@ -0,0 +1,9 @@
1
+ import { checkAbi, describeAbi } from "./abi.js";
2
+ const status = checkAbi();
3
+ process.stdout.write(`${JSON.stringify({
4
+ execPath: process.execPath,
5
+ version: process.version,
6
+ ok: status.ok,
7
+ failure: status.failure,
8
+ detail: describeAbi(status),
9
+ })}\n`);
@@ -0,0 +1,51 @@
1
+ import { cutToUnitBudget, flatten } from "./slug.js";
2
+ const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled", "canceled", "killed", "error"]);
3
+ // Every background_tasks[].type ever observed in a Stop payload, and what hive does with it. "latch"
4
+ // withholds the worker's idle; "name" only names it in a standing notice. An unlisted type is
5
+ // treated as "name" - see unknownTypeIsNamedNotLatched below.
6
+ export const BACKGROUND_TASK_DISPOSITION = {
7
+ subagent: "latch",
8
+ shell: "name",
9
+ monitor: "name",
10
+ };
11
+ export const STALL_BOUND_SECONDS = 15 * 60;
12
+ // Derived, not copied: past the stall bound hive already treats a latched worker as stall-worthy,
13
+ // so that is the ceiling on how long a live subagent may explain one. Keep the reference, not the
14
+ // literal - see .claude/skills/hive-internals/references/worker-state.md.
15
+ export const SUBAGENT_LATCH_MAX_AGE_SECONDS = STALL_BOUND_SECONDS;
16
+ export const SUBAGENT_LATCH_SQL = `-${SUBAGENT_LATCH_MAX_AGE_SECONDS} seconds`;
17
+ export function liveBackgroundTasks(tasks) {
18
+ if (!Array.isArray(tasks))
19
+ return [];
20
+ const live = [];
21
+ for (const entry of tasks) {
22
+ const task = entry;
23
+ const status = String(task?.status ?? "");
24
+ if (TERMINAL_STATUSES.has(status))
25
+ continue;
26
+ live.push({
27
+ type: String(task?.type ?? ""),
28
+ status,
29
+ description: String(task?.description ?? ""),
30
+ });
31
+ }
32
+ return live;
33
+ }
34
+ export const withholdsIdle = (task) => BACKGROUND_TASK_DISPOSITION[task.type] === "latch";
35
+ const typeOf = (task) => flatten(task.type) || "unknown";
36
+ const groupsOf = (tasks) => [...new Set(tasks.map(typeOf))].sort();
37
+ export function describeLiveTasks(tasks) {
38
+ const types = groupsOf(tasks);
39
+ const plural = tasks.length === 1 ? "" : "s";
40
+ return types.length === 1
41
+ ? `${tasks.length} background ${types[0]}${plural}`
42
+ : `${tasks.length} background task${plural} (${types.join(", ")})`;
43
+ }
44
+ const DESCRIPTION_BUDGET = 80;
45
+ export function describeOneTask(task) {
46
+ const flat = flatten(task.description);
47
+ if (flat === "")
48
+ return typeOf(task);
49
+ const text = flat.length > DESCRIPTION_BUDGET ? `${cutToUnitBudget(flat, DESCRIPTION_BUDGET)}…` : flat;
50
+ return `${typeOf(task)}: "${text}"`;
51
+ }