@shanesaravia/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 (47) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +417 -0
  4. package/dist/bin/hive-emit.js +75 -0
  5. package/dist/bin/hive.js +506 -0
  6. package/node_modules/@hive/shared/dist/index.d.ts +2 -0
  7. package/node_modules/@hive/shared/dist/index.js +2 -0
  8. package/node_modules/@hive/shared/dist/status.d.ts +12 -0
  9. package/node_modules/@hive/shared/dist/status.js +52 -0
  10. package/node_modules/@hive/shared/dist/types.d.ts +384 -0
  11. package/node_modules/@hive/shared/dist/types.js +14 -0
  12. package/node_modules/@hive/shared/package.json +18 -0
  13. package/package.json +72 -0
  14. package/packages/server/dist/api/rest.js +793 -0
  15. package/packages/server/dist/api/ws.js +37 -0
  16. package/packages/server/dist/config.js +24 -0
  17. package/packages/server/dist/control/codexRuntime.js +169 -0
  18. package/packages/server/dist/control/killer.js +25 -0
  19. package/packages/server/dist/control/launcher.js +114 -0
  20. package/packages/server/dist/control/messaging.js +75 -0
  21. package/packages/server/dist/control/nativeCommands.js +29 -0
  22. package/packages/server/dist/control/permissionPark.js +23 -0
  23. package/packages/server/dist/control/providerModels.js +53 -0
  24. package/packages/server/dist/events/eventsStore.js +55 -0
  25. package/packages/server/dist/health/deriveAlerts.js +55 -0
  26. package/packages/server/dist/hooks/hookIngest.js +90 -0
  27. package/packages/server/dist/hooks/hookSpool.js +33 -0
  28. package/packages/server/dist/hooks/setupHooks.js +102 -0
  29. package/packages/server/dist/index.js +88 -0
  30. package/packages/server/dist/messages/messagesStore.js +211 -0
  31. package/packages/server/dist/missions/missionsStore.js +283 -0
  32. package/packages/server/dist/paths/pathResolver.js +167 -0
  33. package/packages/server/dist/plans/plansStore.js +212 -0
  34. package/packages/server/dist/policies/policiesStore.js +61 -0
  35. package/packages/server/dist/reports/githubPublisher.js +21 -0
  36. package/packages/server/dist/reports/missionReport.js +16 -0
  37. package/packages/server/dist/roster/rosterBuilder.js +243 -0
  38. package/packages/server/dist/security/originPolicy.js +31 -0
  39. package/packages/server/dist/skills/skillDiscovery.js +69 -0
  40. package/packages/server/dist/templates/templateDiscovery.js +97 -0
  41. package/packages/server/dist/watch/jobsWatcher.js +224 -0
  42. package/packages/server/dist/watch/sessionsWatcher.js +65 -0
  43. package/packages/web/dist/assets/index-CrKMFCkZ.js +11 -0
  44. package/packages/web/dist/assets/index-gEGU_lr3.css +2 -0
  45. package/packages/web/dist/favicon.svg +12 -0
  46. package/packages/web/dist/index.html +14 -0
  47. package/templates/agents/hive-orchestrator.md +42 -0
@@ -0,0 +1,506 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ const currentFile = fileURLToPath(import.meta.url);
8
+ const sourceMode = currentFile.endsWith(".ts");
9
+ const packageRoot = path.resolve(path.dirname(currentFile), sourceMode ? ".." : "../..");
10
+ const defaultPort = 4317;
11
+ export function resolveUserPaths(input = {}) {
12
+ const home = input.home ?? os.homedir();
13
+ const platform = input.platform ?? process.platform;
14
+ const env = input.env ?? process.env;
15
+ const legacyRoot = path.join(home, ".claude-hive");
16
+ const legacyExists = input.legacyExists ?? fs.existsSync(legacyRoot);
17
+ const overrides = (defaults) => ({
18
+ ...defaults,
19
+ config: path.resolve(env.HIVE_CONFIG_DIR ?? defaults.config),
20
+ state: path.resolve(env.HIVE_STATE_DIR ?? defaults.state),
21
+ logs: path.resolve(env.HIVE_LOG_DIR ?? defaults.logs),
22
+ });
23
+ if (env.HIVE_DATA_DIR) {
24
+ const data = path.resolve(env.HIVE_DATA_DIR);
25
+ return overrides({
26
+ data,
27
+ config: data,
28
+ state: data,
29
+ logs: path.join(data, "logs"),
30
+ legacy: false,
31
+ });
32
+ }
33
+ if (legacyExists) {
34
+ return overrides({ data: legacyRoot, config: legacyRoot, state: legacyRoot, logs: path.join(legacyRoot, "logs"), legacy: true });
35
+ }
36
+ if (platform === "darwin") {
37
+ const data = path.join(home, "Library", "Application Support", "Hive");
38
+ return overrides({ data, config: data, state: data, logs: path.join(home, "Library", "Logs", "Hive"), legacy: false });
39
+ }
40
+ if (platform === "win32") {
41
+ const data = path.join(env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Hive");
42
+ const config = path.join(env.APPDATA ?? path.join(home, "AppData", "Roaming"), "Hive");
43
+ return overrides({ data, config, state: data, logs: path.join(data, "logs"), legacy: false });
44
+ }
45
+ const data = path.join(env.XDG_DATA_HOME ?? path.join(home, ".local", "share"), "hive");
46
+ const config = path.join(env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "hive");
47
+ const state = path.join(env.XDG_STATE_HOME ?? path.join(home, ".local", "state"), "hive");
48
+ return overrides({ data, config, state, logs: path.join(state, "logs"), legacy: false });
49
+ }
50
+ function userPaths() {
51
+ return resolveUserPaths();
52
+ }
53
+ function dataDir() {
54
+ return userPaths().data;
55
+ }
56
+ function runtimePath() {
57
+ return path.join(userPaths().state, "runtime.json");
58
+ }
59
+ function configPath() {
60
+ return path.join(userPaths().config, "config.json");
61
+ }
62
+ function logPath() {
63
+ return path.join(userPaths().logs, "hive.log");
64
+ }
65
+ function readCliConfig() {
66
+ try {
67
+ return JSON.parse(fs.readFileSync(configPath(), "utf8"));
68
+ }
69
+ catch {
70
+ return {};
71
+ }
72
+ }
73
+ function writeCliConfig(config) {
74
+ fs.mkdirSync(path.dirname(configPath()), { recursive: true });
75
+ const temporary = `${configPath()}.tmp-${process.pid}`;
76
+ fs.writeFileSync(temporary, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
77
+ fs.renameSync(temporary, configPath());
78
+ }
79
+ function serverEntry() {
80
+ return path.join(packageRoot, "packages", "server", sourceMode ? path.join("src", "index.ts") : path.join("dist", "index.js"));
81
+ }
82
+ function webEntry() {
83
+ return path.join(packageRoot, "packages", "web", "dist", "index.html");
84
+ }
85
+ function templatePath() {
86
+ return path.join(packageRoot, "templates", "agents", "hive-orchestrator.md");
87
+ }
88
+ function emitEntry() {
89
+ return path.join(packageRoot, sourceMode ? "bin" : path.join("dist", "bin"), sourceMode ? "hive-emit.ts" : "hive-emit.js");
90
+ }
91
+ function packageVersion() {
92
+ try {
93
+ const value = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
94
+ return value.version ?? "0.0.0";
95
+ }
96
+ catch {
97
+ return "0.0.0";
98
+ }
99
+ }
100
+ function shellQuote(value) {
101
+ return `'${value.replaceAll("'", "'\\''")}'`;
102
+ }
103
+ function modulePath(compiled, source) {
104
+ return path.join(packageRoot, "packages", "server", sourceMode ? source : compiled);
105
+ }
106
+ async function importRuntimeModule(compiled, source) {
107
+ const target = modulePath(compiled, source);
108
+ if (!fs.existsSync(target)) {
109
+ throw new Error(`Required Hive runtime module is missing at ${target}. Run \`npm run build\` first.`);
110
+ }
111
+ return import(pathToFileURL(target).href);
112
+ }
113
+ export function isSupportedNodeVersion(version = process.versions.node) {
114
+ return Number(version.split(".")[0]) >= 20;
115
+ }
116
+ export function parseStartOptions(args) {
117
+ let port = Number(process.env.HIVE_PORT ?? readCliConfig().port ?? defaultPort);
118
+ let open = true;
119
+ for (let index = 0; index < args.length; index++) {
120
+ const arg = args[index];
121
+ if (arg === "--no-open")
122
+ open = false;
123
+ else if (arg === "--port") {
124
+ port = Number(args[++index]);
125
+ }
126
+ else {
127
+ throw new Error(`Unknown start option: ${arg}`);
128
+ }
129
+ }
130
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
131
+ throw new Error("Port must be an integer between 1 and 65535.");
132
+ }
133
+ return { port, open };
134
+ }
135
+ function readRuntimeState() {
136
+ try {
137
+ const state = JSON.parse(fs.readFileSync(runtimePath(), "utf8"));
138
+ return Number.isInteger(state.pid) && Number.isInteger(state.port) ? state : undefined;
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ }
144
+ function writeRuntimeState(state) {
145
+ fs.mkdirSync(path.dirname(runtimePath()), { recursive: true });
146
+ fs.writeFileSync(runtimePath(), `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
147
+ }
148
+ function clearRuntimeState(pid) {
149
+ const state = readRuntimeState();
150
+ if (pid !== undefined && state?.pid !== pid)
151
+ return;
152
+ try {
153
+ fs.unlinkSync(runtimePath());
154
+ }
155
+ catch (error) {
156
+ if (error.code !== "ENOENT")
157
+ throw error;
158
+ }
159
+ }
160
+ function processExists(pid) {
161
+ try {
162
+ process.kill(pid, 0);
163
+ return true;
164
+ }
165
+ catch (error) {
166
+ return error.code === "EPERM";
167
+ }
168
+ }
169
+ async function health(port, timeoutMs = 800) {
170
+ try {
171
+ const controller = new AbortController();
172
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
173
+ const response = await fetch(`http://127.0.0.1:${port}/health`, { signal: controller.signal });
174
+ clearTimeout(timeout);
175
+ return response.ok;
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ }
181
+ async function waitForHealth(port, child, timeoutMs = 12_000) {
182
+ const deadline = Date.now() + timeoutMs;
183
+ while (Date.now() < deadline) {
184
+ if (child.exitCode !== null)
185
+ throw new Error(`Hive server exited during startup with code ${child.exitCode}.`);
186
+ if (await health(port))
187
+ return;
188
+ await new Promise((resolve) => setTimeout(resolve, 150));
189
+ }
190
+ throw new Error(`Hive did not become healthy on port ${port} within ${timeoutMs / 1000} seconds.`);
191
+ }
192
+ function launchBrowser(url) {
193
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
194
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
195
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
196
+ child.on("error", () => console.warn(`Could not open a browser automatically. Open ${url}`));
197
+ child.unref();
198
+ }
199
+ async function runStart(args, openByDefault = true) {
200
+ const options = parseStartOptions(openByDefault ? args : ["--no-open", ...args]);
201
+ const url = `http://127.0.0.1:${options.port}`;
202
+ const state = readRuntimeState();
203
+ if (await health(options.port)) {
204
+ console.log(`Hive is already running at ${url}`);
205
+ if (options.open)
206
+ launchBrowser(url);
207
+ return;
208
+ }
209
+ if (state && !processExists(state.pid))
210
+ clearRuntimeState(state.pid);
211
+ if (!isSupportedNodeVersion()) {
212
+ throw new Error(`Hive requires Node.js 20 or newer; found ${process.versions.node}.`);
213
+ }
214
+ if (!fs.existsSync(serverEntry()) || (!sourceMode && !fs.existsSync(webEntry()))) {
215
+ throw new Error("Hive production assets are missing. Run `npm run build` before starting Hive.");
216
+ }
217
+ if (!readCliConfig().setupCompletedAt) {
218
+ console.log("First-run provider setup is optional. Run `hive setup` to preview it; Hive will start without changing provider configuration.");
219
+ }
220
+ fs.mkdirSync(dataDir(), { recursive: true });
221
+ fs.mkdirSync(path.dirname(logPath()), { recursive: true });
222
+ const log = fs.createWriteStream(logPath(), { flags: "a", mode: 0o600 });
223
+ log.write(`\n--- Hive ${packageVersion()} started ${new Date().toISOString()} on ${url} ---\n`);
224
+ const executable = sourceMode ? path.join(packageRoot, "node_modules", ".bin", "tsx") : process.execPath;
225
+ const child = spawn(executable, [serverEntry()], {
226
+ env: {
227
+ ...process.env,
228
+ HIVE_DATA_DIR: dataDir(),
229
+ HIVE_PORT: String(options.port),
230
+ HIVE_DASHBOARD_URL: url,
231
+ NODE_ENV: sourceMode ? process.env.NODE_ENV : "production",
232
+ },
233
+ stdio: ["inherit", "pipe", "pipe"],
234
+ });
235
+ child.stdout.pipe(process.stdout);
236
+ child.stderr.pipe(process.stderr);
237
+ child.stdout.pipe(log, { end: false });
238
+ child.stderr.pipe(log, { end: false });
239
+ const forward = (signal) => {
240
+ if (child.exitCode === null)
241
+ child.kill(signal);
242
+ };
243
+ process.once("SIGINT", forward);
244
+ process.once("SIGTERM", forward);
245
+ try {
246
+ await waitForHealth(options.port, child);
247
+ writeRuntimeState({ pid: child.pid, port: options.port, startedAt: new Date().toISOString(), version: packageVersion() });
248
+ console.log(`Hive is running at ${url}`);
249
+ if (options.open)
250
+ launchBrowser(url);
251
+ const exitCode = await new Promise((resolve, reject) => {
252
+ child.once("error", reject);
253
+ child.once("exit", (code) => resolve(code ?? 0));
254
+ });
255
+ if (exitCode)
256
+ process.exitCode = exitCode;
257
+ }
258
+ catch (error) {
259
+ if (child.exitCode === null)
260
+ child.kill("SIGTERM");
261
+ throw error;
262
+ }
263
+ finally {
264
+ process.removeListener("SIGINT", forward);
265
+ process.removeListener("SIGTERM", forward);
266
+ clearRuntimeState(child.pid);
267
+ log.end();
268
+ }
269
+ }
270
+ function renderedPersona() {
271
+ const source = templatePath();
272
+ if (!fs.existsSync(source) || !fs.existsSync(emitEntry())) {
273
+ throw new Error("Hive setup assets are missing. Run `npm run build` first.");
274
+ }
275
+ const destination = path.join(os.homedir(), ".claude", "agents", "hive-orchestrator.md");
276
+ const emitCommand = sourceMode
277
+ ? `${shellQuote(path.join(packageRoot, "node_modules", ".bin", "tsx"))} ${shellQuote(emitEntry())}`
278
+ : `${shellQuote(process.execPath)} ${shellQuote(emitEntry())}`;
279
+ const rendered = fs.readFileSync(source, "utf8").replaceAll("{{HIVE_EMIT_CMD}}", emitCommand);
280
+ const existing = fs.existsSync(destination) ? fs.readFileSync(destination, "utf8") : undefined;
281
+ return { destination, content: rendered, changed: existing !== rendered };
282
+ }
283
+ function installPersona(persona) {
284
+ fs.mkdirSync(path.dirname(persona.destination), { recursive: true });
285
+ fs.writeFileSync(persona.destination, persona.content);
286
+ }
287
+ async function runSetup(assumeYes) {
288
+ process.env.HIVE_DATA_DIR ??= dataDir();
289
+ const persona = renderedPersona();
290
+ const hooks = await importRuntimeModule("dist/hooks/setupHooks.js", "src/hooks/setupHooks.ts");
291
+ const hookPlan = hooks.planHookSetup();
292
+ console.log("Hive setup plan:");
293
+ console.log(` ${persona.changed ? "+" : "✓"} Claude orchestrator persona: ${persona.destination}`);
294
+ console.log(` ${hookPlan.changed ? "+" : "✓"} Additive Claude event hooks: ${path.join(os.homedir(), ".claude", "settings.json")}`);
295
+ console.log(` • Hive data: ${dataDir()}${userPaths().legacy ? " (existing legacy location preserved)" : ""}`);
296
+ if (!persona.changed && !hookPlan.changed && readCliConfig().setupCompletedAt) {
297
+ console.log("Setup is already complete — nothing to change.");
298
+ return;
299
+ }
300
+ if (!assumeYes) {
301
+ console.log("No files were changed. Existing hooks are preserved and settings are backed up before any hook changes.");
302
+ console.log("Re-run with --yes to apply.");
303
+ return;
304
+ }
305
+ if (persona.changed)
306
+ installPersona(persona);
307
+ if (hookPlan.changed)
308
+ hooks.applyHookSetup();
309
+ writeCliConfig({ ...readCliConfig(), setupCompletedAt: new Date().toISOString() });
310
+ console.log("Hive provider setup is complete.");
311
+ }
312
+ async function runStatus() {
313
+ const state = readRuntimeState();
314
+ const port = state?.port ?? Number(process.env.HIVE_PORT ?? defaultPort);
315
+ // Initial repository/session discovery can briefly occupy the server after it
316
+ // starts. Give an interactive status check enough time to get a response
317
+ // instead of reporting a healthy process as offline under CI or on large homes.
318
+ const healthy = await health(port, 3_000);
319
+ if (healthy) {
320
+ console.log(`Hive is running at http://127.0.0.1:${port}${state ? ` (pid ${state.pid})` : ""}.`);
321
+ return;
322
+ }
323
+ if (state && !processExists(state.pid))
324
+ clearRuntimeState(state.pid);
325
+ console.log("Hive is not running.");
326
+ process.exitCode = 1;
327
+ }
328
+ async function runOpen() {
329
+ const state = readRuntimeState();
330
+ const port = state?.port ?? Number(process.env.HIVE_PORT ?? defaultPort);
331
+ if (!(await health(port, 3_000)))
332
+ throw new Error("Hive is not running. Start it with `hive`.");
333
+ const url = `http://127.0.0.1:${port}`;
334
+ launchBrowser(url);
335
+ console.log(`Opened ${url}`);
336
+ }
337
+ async function runStop() {
338
+ const state = readRuntimeState();
339
+ if (!state || !processExists(state.pid)) {
340
+ if (state)
341
+ clearRuntimeState(state.pid);
342
+ console.log("Hive is not running.");
343
+ return;
344
+ }
345
+ process.kill(state.pid, "SIGTERM");
346
+ const deadline = Date.now() + 5_000;
347
+ while (Date.now() < deadline && processExists(state.pid)) {
348
+ await new Promise((resolve) => setTimeout(resolve, 100));
349
+ }
350
+ if (processExists(state.pid))
351
+ throw new Error(`Hive process ${state.pid} did not stop gracefully.`);
352
+ clearRuntimeState(state.pid);
353
+ console.log("Hive stopped.");
354
+ }
355
+ function commandVersion(command) {
356
+ const result = spawnSync(command, ["--version"], { encoding: "utf8", timeout: 3_000 });
357
+ if (result.error || result.status !== 0)
358
+ return undefined;
359
+ return (result.stdout || result.stderr).trim().split("\n")[0];
360
+ }
361
+ function providerAuthentication(command) {
362
+ if (command === "claude") {
363
+ const result = spawnSync(command, ["auth", "status", "--json"], { encoding: "utf8", timeout: 3_000 });
364
+ if (result.error)
365
+ return { ok: false, detail: "authentication status unavailable" };
366
+ try {
367
+ const status = JSON.parse(result.stdout);
368
+ return status.loggedIn
369
+ ? { ok: true, detail: `authenticated${status.authMethod ? ` (${status.authMethod})` : ""}` }
370
+ : { ok: false, detail: "not authenticated" };
371
+ }
372
+ catch {
373
+ return { ok: false, detail: "authentication status unavailable" };
374
+ }
375
+ }
376
+ const result = spawnSync(command, ["login", "status"], { encoding: "utf8", timeout: 3_000 });
377
+ return result.status === 0
378
+ ? { ok: true, detail: result.stdout.trim().split("\n")[0] || "authenticated" }
379
+ : { ok: false, detail: "not authenticated" };
380
+ }
381
+ async function runDoctor() {
382
+ const checks = [];
383
+ checks.push({ ok: isSupportedNodeVersion(), required: true, label: "Node.js", detail: process.versions.node });
384
+ checks.push({ ok: fs.existsSync(serverEntry()), required: true, label: "Server build", detail: serverEntry() });
385
+ checks.push({ ok: sourceMode || fs.existsSync(webEntry()), required: true, label: "Web build", detail: webEntry() });
386
+ try {
387
+ fs.mkdirSync(dataDir(), { recursive: true });
388
+ fs.accessSync(dataDir(), fs.constants.R_OK | fs.constants.W_OK);
389
+ checks.push({ ok: true, required: true, label: "Data directory", detail: dataDir() });
390
+ }
391
+ catch (error) {
392
+ checks.push({ ok: false, required: true, label: "Data directory", detail: error.message });
393
+ }
394
+ for (const provider of ["claude", "codex"]) {
395
+ const version = commandVersion(provider);
396
+ checks.push({ ok: Boolean(version), required: false, label: `${provider[0].toUpperCase()}${provider.slice(1)} CLI`, detail: version ?? "not found (optional)" });
397
+ if (version) {
398
+ const authentication = providerAuthentication(provider);
399
+ checks.push({ ok: authentication.ok, required: false, label: `${provider[0].toUpperCase()}${provider.slice(1)} auth`, detail: authentication.detail });
400
+ }
401
+ }
402
+ const state = readRuntimeState();
403
+ const port = state?.port ?? Number(process.env.HIVE_PORT ?? defaultPort);
404
+ checks.push({ ok: await health(port), required: false, label: "Hive runtime", detail: `http://127.0.0.1:${port}` });
405
+ checks.push({ ok: true, required: false, label: "Configuration", detail: configPath() });
406
+ checks.push({ ok: true, required: false, label: "Runtime state", detail: runtimePath() });
407
+ checks.push({ ok: true, required: false, label: "Logs", detail: logPath() });
408
+ for (const check of checks) {
409
+ console.log(`${check.ok ? "✓" : check.required ? "✗" : "○"} ${check.label}: ${check.detail}`);
410
+ }
411
+ const failures = checks.filter((check) => check.required && !check.ok);
412
+ const providers = checks.filter((check) => check.label.endsWith(" CLI") && check.ok);
413
+ const authenticatedProviders = checks.filter((check) => check.label.endsWith(" auth") && check.ok);
414
+ if (!providers.length)
415
+ console.log("\nInstall and authenticate Claude Code or Codex before launching a mission.");
416
+ else if (!authenticatedProviders.length)
417
+ console.log("\nAuthenticate at least one installed provider CLI before launching a mission.");
418
+ if (failures.length) {
419
+ process.exitCode = 1;
420
+ console.log(`\n${failures.length} required check${failures.length === 1 ? "" : "s"} failed.`);
421
+ }
422
+ else {
423
+ console.log("\nHive is ready.");
424
+ }
425
+ }
426
+ async function runOrchestratorStart(args) {
427
+ const worktreeIndex = args.indexOf("--worktree");
428
+ const nameIndex = args.indexOf("--name");
429
+ let worktree;
430
+ if (worktreeIndex !== -1) {
431
+ worktree = true;
432
+ args.splice(worktreeIndex, 1);
433
+ }
434
+ let name;
435
+ if (nameIndex !== -1) {
436
+ name = args[nameIndex + 1];
437
+ args.splice(nameIndex, 2);
438
+ }
439
+ const task = args.join(" ").trim();
440
+ if (!task)
441
+ throw new Error('Usage: hive orchestrator start "<task>" [--worktree] [--name <name>]');
442
+ const launcher = await importRuntimeModule("dist/control/launcher.js", "src/control/launcher.ts");
443
+ const result = await launcher.startOrchestrator({ task, worktree, name });
444
+ console.log(`Started orchestrator pid=${result.pid}`);
445
+ }
446
+ function printHelp() {
447
+ console.log(`Hive ${packageVersion()} — provider-neutral mission control
448
+
449
+ Usage:
450
+ hive [start] [--port <port>] [--no-open] Start Hive and open the dashboard
451
+ hive doctor Check local requirements and providers
452
+ hive setup [--yes] Install the orchestrator persona and hooks
453
+ hive status Show whether Hive is running
454
+ hive open Open the running dashboard
455
+ hive stop Gracefully stop a recorded Hive server
456
+ hive --version Print the installed version
457
+ hive --help Show this help
458
+
459
+ Compatibility:
460
+ hive serve [--port <port>] Start Hive without opening a browser
461
+ hive orchestrator start "<task>" [--worktree] [--name <name>]`);
462
+ }
463
+ export async function main(argv = process.argv.slice(2)) {
464
+ const [command, ...args] = argv;
465
+ switch (command) {
466
+ case undefined:
467
+ case "start": return runStart(args);
468
+ case "serve": return runStart(args, false);
469
+ case "doctor": return runDoctor();
470
+ case "setup": return runSetup(args.includes("--yes"));
471
+ case "status": return runStatus();
472
+ case "open": return runOpen();
473
+ case "stop": return runStop();
474
+ case "--version":
475
+ case "-v":
476
+ console.log(packageVersion());
477
+ return;
478
+ case "--help":
479
+ case "-h":
480
+ case "help":
481
+ printHelp();
482
+ return;
483
+ case "orchestrator":
484
+ if (args[0] === "start")
485
+ return runOrchestratorStart(args.slice(1));
486
+ break;
487
+ }
488
+ printHelp();
489
+ throw new Error(`Unknown Hive command: ${command}`);
490
+ }
491
+ function isMainModule(argvEntry) {
492
+ if (!argvEntry)
493
+ return false;
494
+ try {
495
+ return fs.realpathSync(argvEntry) === fs.realpathSync(currentFile);
496
+ }
497
+ catch {
498
+ return path.resolve(argvEntry) === currentFile;
499
+ }
500
+ }
501
+ if (isMainModule(process.argv[1])) {
502
+ main().catch((error) => {
503
+ console.error(`Hive error: ${error.message}`);
504
+ process.exit(1);
505
+ });
506
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./status.js";
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./status.js";
@@ -0,0 +1,12 @@
1
+ import type { ClaudeJob, ClaudeSession, DerivedStatus, HiveEvent } from "./types.js";
2
+ /**
3
+ * Pure status derivation — no network/LLM calls. Joins the raw session/job
4
+ * records already on disk. A job still marked "working" with no matching
5
+ * live session file is flagged stale/error rather than silently dropped,
6
+ * since that's the one case pure file-watching can otherwise miss (the
7
+ * process died without a clean shutdown).
8
+ */
9
+ export declare function deriveStatus(session: ClaudeSession | undefined, job: ClaudeJob | undefined, recentEvents?: HiveEvent[]): {
10
+ status: DerivedStatus;
11
+ stale: boolean;
12
+ };
@@ -0,0 +1,52 @@
1
+ const STALE_JOB_STATES = new Set(["working", "busy"]);
2
+ /**
3
+ * Pure status derivation — no network/LLM calls. Joins the raw session/job
4
+ * records already on disk. A job still marked "working" with no matching
5
+ * live session file is flagged stale/error rather than silently dropped,
6
+ * since that's the one case pure file-watching can otherwise miss (the
7
+ * process died without a clean shutdown).
8
+ */
9
+ export function deriveStatus(session, job, recentEvents = []) {
10
+ // The daemon's idle notification ("Claude is waiting for your input") fires
11
+ // after every completed turn — it means "session idle", not "the assistant
12
+ // asked you something", so it must not read as waiting_on_you. Permission
13
+ // notifications and emitted blocked_on_user decisions still do.
14
+ const latestDecisionEvent = [...recentEvents].reverse().find((e) => e.phase === "blocked_on_user" || e.phase === "decision_resolved"
15
+ || (e.hookEventName === "Notification" && !/waiting for your input/i.test(e.detail ?? "")));
16
+ const blockedEvent = latestDecisionEvent?.phase !== "decision_resolved" ? latestDecisionEvent : undefined;
17
+ // An inferred wait (a "waiting for your input" Notification or an emitted
18
+ // blocked_on_user) is only current while nothing newer happened. The daemon
19
+ // fires idle Notifications even while subagents run, and activity resuming
20
+ // after one proves the session isn't parked on the user.
21
+ const latestEventTs = recentEvents.at(-1)?.ts ?? 0;
22
+ const blockedEventIsCurrent = blockedEvent !== undefined && blockedEvent.ts >= latestEventTs;
23
+ if (!session && job && STALE_JOB_STATES.has(job.state)) {
24
+ return { status: "error", stale: true };
25
+ }
26
+ if (!session) {
27
+ return { status: job?.state === "done" ? "done" : "idle", stale: false };
28
+ }
29
+ if (session.status === "waiting") {
30
+ return { status: "waiting_on_you", stale: false };
31
+ }
32
+ if (job?.state === "blocked") {
33
+ return { status: "waiting_on_you", stale: false };
34
+ }
35
+ // Mid-turn park: a permission prompt keeps state "working" but the daemon
36
+ // flips tempo to blocked and puts the ask in needs ("approve Bash: …").
37
+ if (job?.tempo === "blocked" && job.needs) {
38
+ return { status: "waiting_on_you", stale: false };
39
+ }
40
+ // A busy session outranks event-inferred waiting: the runtime is provably
41
+ // doing work (e.g. orchestrating subagents), not parked on the user.
42
+ if (session.status === "busy") {
43
+ return { status: "working", stale: false };
44
+ }
45
+ if (blockedEventIsCurrent) {
46
+ return { status: "waiting_on_you", stale: false };
47
+ }
48
+ if (job?.state === "done") {
49
+ return { status: "done", stale: false };
50
+ }
51
+ return { status: "idle", stale: false };
52
+ }