@shanesaravia/hive 0.3.0 → 0.4.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 (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +16 -1
  3. package/node_modules/@hive/shared/dist/status.js +9 -0
  4. package/node_modules/@hive/shared/dist/types.d.ts +162 -1
  5. package/node_modules/@hive/shared/dist/types.js +27 -0
  6. package/package.json +1 -1
  7. package/packages/server/dist/agents/agentDiscovery.js +64 -0
  8. package/packages/server/dist/api/rest.js +474 -14
  9. package/packages/server/dist/api/ws.js +66 -2
  10. package/packages/server/dist/control/missionQuiesce.js +66 -0
  11. package/packages/server/dist/health/deriveAlerts.js +8 -0
  12. package/packages/server/dist/index.js +26 -2
  13. package/packages/server/dist/loops/loopCommand.js +56 -0
  14. package/packages/server/dist/loops/loopNoop.js +38 -0
  15. package/packages/server/dist/loops/loopScheduler.js +58 -0
  16. package/packages/server/dist/loops/loopStore.js +118 -0
  17. package/packages/server/dist/loops/monitors.js +38 -0
  18. package/packages/server/dist/messages/attachmentStore.js +92 -0
  19. package/packages/server/dist/messages/messagesStore.js +97 -33
  20. package/packages/server/dist/reviews/reviewDiff.js +47 -0
  21. package/packages/server/dist/roster/replyAsk.js +62 -0
  22. package/packages/server/dist/roster/rosterBuilder.js +41 -5
  23. package/packages/server/dist/roster/workerIdentity.js +46 -9
  24. package/packages/server/dist/skills/skillDiscovery.js +28 -4
  25. package/packages/server/dist/terminals/claudeStreamClient.js +90 -0
  26. package/packages/server/dist/terminals/codexAppServerClient.js +195 -0
  27. package/packages/server/dist/terminals/providerDetection.js +27 -0
  28. package/packages/server/dist/terminals/terminalCapability.js +45 -0
  29. package/packages/server/dist/terminals/terminalFeatures.js +11 -0
  30. package/packages/server/dist/terminals/terminalObservability.js +21 -0
  31. package/packages/server/dist/terminals/terminalRuntime.js +125 -0
  32. package/packages/server/dist/terminals/terminalStream.js +30 -0
  33. package/packages/server/dist/transcripts/transcriptReader.js +345 -0
  34. package/packages/web/dist/assets/index-DWjqiitn.js +17 -0
  35. package/packages/web/dist/assets/index-rd4RnLqj.css +2 -0
  36. package/packages/web/dist/index.html +2 -2
  37. package/packages/web/dist/assets/index-BpEYVjCF.css +0 -2
  38. package/packages/web/dist/assets/index-rIAIJyuF.js +0 -12
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to Hive will be documented in this file.
4
4
 
5
+ ## 0.4.0 — 2026-09-02
6
+
7
+ Makes the conversation trustworthy to read, gives Hive loops it can actually turn off, and settles two places where a mission misreported what it was doing.
8
+
9
+ ### Added
10
+
11
+ - Images pasted or dropped into a mission conversation. They upload as you type rather than on send, render as thumbnails in the message, and reach the agent as a path on disk to read — the one route that behaves the same in a repository, a worktree, or anywhere else.
12
+ - `@agent` completion in the composer, from the `.claude/agents` definitions, by the name each declares rather than its filename, with a project definition shadowing a user one exactly as the CLI resolves them.
13
+ - Loops Hive owns: `/loop 10m …` registers a recurring prompt Hive re-sends, kept in the database so it survives a restart instead of dying quietly while it still looks like it is running. `/loop list`, `/loop stop [id]` and `/tasks` in any conversation; a "N running" pill in the header, drawn only when something is, opening a panel that lists every loop and background monitor across the fleet with a stop on each.
14
+ - Consecutive loop passes that find nothing fold into one row in the transcript, so a poller that finds nothing overnight cannot bury the two runs that mattered. The row expands to the individual passes.
15
+
16
+ ### Fixed
17
+
18
+ - A message could appear and then be replaced by a different one. A turn's reply row was rewritten on every poll as the turn progressed, progress notes were keyed by list position so a filtered entry slid every following one onto other text, and timestamps were rewritten, moving already-read messages. Text now only ever grows in place; anything genuinely different is appended.
19
+ - New messages no longer pull the viewport to the bottom. Scrolled up, you keep your place and are offered the way back.
20
+ - A decision no longer replaces the conversation it refers to. It is drawn inline where it was raised, with one line above the transcript that flags an unaddressed decision and walks between them when there are several.
21
+ - A turn that ends by asking you something — rather than through a structured prompt — is now an answerable decision instead of a mission that looks finished while it waits.
22
+ - A mission whose workers were running reported "Idle": a manager's session reads idle between its own turns while the subagents it dispatched keep working.
23
+ - Skill and agent discovery no longer fail outright when a mission's recorded repository is gone, which is every mission whose worktree has been reclaimed. Your own skills and agents apply regardless of where a mission runs. This also made such a mission impossible to message at all, because the send path discovered skills before falling back to a live repository.
24
+ - The composer takes focus when a mission opens.
25
+
5
26
  ## 0.3.0 — 2026-08-29
6
27
 
7
28
  Turns a mission's full page into a dashboard, carries the keyboard into it, and settles a run of places where two parts of the board disagreed about one fact.
package/README.md CHANGED
@@ -179,12 +179,14 @@ The selected working directory matters. The provider CLI is launched from that d
179
179
 
180
180
  ## Continue a conversation
181
181
 
182
- Open a mission card and send a follow-up from the Conversation panel. Both providers preserve their native conversation/thread identity while Hive records each turn under the same durable mission.
182
+ Open a mission card and send a follow-up from the Conversation panel. Both providers preserve their native conversation/thread identity while Hive records each turn under the same durable mission. Conversation defaults to a quiet narrative that interleaves messages with plan, task, worker, verification, gate, and review milestones; switch to **Messages only** when you want an unaugmented transcript. Historical missions are not given synthetic milestones—stored events are shown as recorded, and future transitions are enriched durably.
183
183
 
184
184
  Type `/` in either the new-mission prompt or an existing conversation to search the skills available to that provider in the selected working directory. Hive reads Claude skills for Claude missions and Codex skills for Codex missions; slash-form Codex selections are translated to the provider's native invocation form behind the scenes. Recognized skills are highlighted and sent as ordinary prompt text, leaving the provider CLI responsible for execution and permissions. Hive discovers skill metadata only; it does not preload entire skill bodies into mission context.
185
185
 
186
186
  If a provider or deliberately interactive skill asks a structured question, Hive surfaces it in **Needs you** and the mission conversation. Answering there resumes the same mission; skill prompts are not silently bypassed.
187
187
 
188
+ **Needs you** also supports focused `j`/`k` triage, snoozing, and deferral until an item materially changes. Awaiting-acceptance missions expose a review packet with observed files, verification, gates, outputs, risks, source-control context, freshness protection, and actions to request changes, add or waive gates, rerun verification, or accept. Pin up to four missions into the persistent workspace with `Shift+Enter` or the command palette; `Cmd/Ctrl+1…4` focuses panes, and pane order, density, and split are retained locally.
189
+
188
190
  ## Navigate the fleet
189
191
 
190
192
  The fleet is organized around attention rather than process creation time:
@@ -284,6 +286,19 @@ Health alerts explain the condition, supporting evidence, and a suggested recove
284
286
 
285
287
  Mission and task controls include pause/resume, graceful or confirmed force stop, manager replacement, retry, cancellation, worker replacement/reassignment, priority changes, plan revision, fresh review, verification pass, completion, reopening, archive, and permanent deletion. Control actions are recorded in the activity timeline.
286
288
 
289
+ ### Conversation and Terminal
290
+
291
+ Conversation is the primary structured mission interface. Terminal provides chronological provider output as a debugging and migration fallback. Claude manager sources currently send clearly labelled provider follow-ups; supported Codex manager sources can start an interactive app-server turn. Worker, completed, unsupported, busy, and disconnected sources stay readable but do not present a writable prompt.
292
+
293
+ Terminal input uses Enter to send and Shift+Enter for a new line. Large pastes require confirmation, control sequences are sanitized from transcript rendering, and interrupting a turn is distinct from pausing or stopping its mission. Search, plain-text copy, source selection, scroll position, and drafts work independently per mission source.
294
+
295
+ Recovery order is: return to Conversation, refresh the durable transcript, reload Hive to reconnect, interrupt the current interactive turn, then use the mission Pause/Stop controls if necessary. Writable modes can be disabled independently without disabling transcript viewing:
296
+
297
+ - `HIVE_TERMINAL_CODEX_INPUT=0` disables Codex interactive Terminal input.
298
+ - `HIVE_TERMINAL_CLAUDE_FOLLOW_UP=0` disables Claude follow-ups from Terminal; Conversation remains available.
299
+
300
+ Hive records local, privacy-safe Terminal audit metadata and aggregate counters—provider, mission/source identity, character count, outcome, reconnects, failures, and cleanup. It never adds raw Terminal input, credentials, or environment values to those records. Current local status is available at `GET /api/terminal/status`.
301
+
287
302
  ## Reports and GitHub
288
303
 
289
304
  The Technical view exports a mission report as Markdown or JSON. Reports include the request, plan, decisions, workers, changed files, verification, outputs, resource use, conversation, and remaining risks. With a locally authenticated `gh` CLI, Hive can post a report to a GitHub issue or pull request after explicit confirmation; it does not publish automatically.
@@ -57,6 +57,15 @@ export function deriveStatus(session, job, recentEvents = [], now = Date.now())
57
57
  if (blockedEventIsCurrent) {
58
58
  return { status: "waiting_on_you", stale: false };
59
59
  }
60
+ // A job the runtime still calls working is working, even while its own
61
+ // session record reads idle. A manager sits quiet between turns while the
62
+ // subagents it dispatched keep running, and that gap was presenting as an
63
+ // idle mission on a board where workers were visibly reporting progress.
64
+ // A job left "working" by a dead process ages out into stalled upstream,
65
+ // where the elapsed time is known.
66
+ if (STALE_JOB_STATES.has(job?.state ?? "")) {
67
+ return { status: "working", stale: false };
68
+ }
60
69
  if (job?.state === "done") {
61
70
  return { status: "done", stale: false };
62
71
  }
@@ -76,6 +76,17 @@ export interface ClaudeJob {
76
76
  backend?: string;
77
77
  template?: string;
78
78
  }
79
+ /** An image pasted into a mission conversation and written to disk for the agent to read. */
80
+ export interface MessageAttachment {
81
+ id: string;
82
+ name: string;
83
+ mediaType: string;
84
+ bytes: number;
85
+ /** Absolute path on the Hive host — what the agent is told to open. */
86
+ path: string;
87
+ /** Server route the web UI renders the thumbnail from. */
88
+ url: string;
89
+ }
79
90
  export interface MissionMessage {
80
91
  id: string;
81
92
  role: "user" | "assistant";
@@ -84,6 +95,13 @@ export interface MissionMessage {
84
95
  jobId: string;
85
96
  /** `update`: something the agent said on the way to its reply, shown compactly. */
86
97
  kind?: "reply" | "update";
98
+ attachments?: MessageAttachment[];
99
+ /**
100
+ * A scheduled loop pass that found nothing to do. Consecutive quiet runs
101
+ * collapse into one row in the transcript so they cannot bury the runs that
102
+ * mattered — the messages themselves are still kept, and still readable.
103
+ */
104
+ noop?: boolean;
87
105
  }
88
106
  /** @deprecated Use MissionMessage. */
89
107
  export type ThreadMessage = MissionMessage;
@@ -184,6 +202,64 @@ export interface ClaudeSkillSummary {
184
202
  argumentHint?: string;
185
203
  source: "project" | "user" | "plugin" | "built-in" | "claude";
186
204
  }
205
+ /**
206
+ * A subagent persona the conversation can address by name with `@qa-engineer`.
207
+ *
208
+ * These are the `.md` files under `.claude/agents`, the same ones the CLI
209
+ * offers — Hive discovers them so the composer can complete them, and passes
210
+ * the mention through untouched for the session's own agent loader to resolve.
211
+ */
212
+ export interface ClaudeAgentSummary {
213
+ name: string;
214
+ description?: string;
215
+ source: "project" | "user";
216
+ }
217
+ /**
218
+ * A recurring prompt Hive re-sends to a mission on an interval.
219
+ *
220
+ * Hive owns the schedule rather than delegating to the agent's own `/loop`:
221
+ * that one lives inside a session's runtime with nothing on disk, so it can be
222
+ * neither listed nor stopped from outside. A loop you cannot turn off is worse
223
+ * than no loop at all.
224
+ */
225
+ export interface MissionLoop {
226
+ id: string;
227
+ missionId: string;
228
+ missionName?: string;
229
+ prompt: string;
230
+ intervalMs: number;
231
+ status: "active" | "stopped";
232
+ createdAt: number;
233
+ /** Absent until the loop has fired at least once. */
234
+ lastRunAt?: number;
235
+ nextRunAt: number;
236
+ runCount: number;
237
+ /** Why it stopped, when it stopped itself rather than being stopped. */
238
+ stoppedReason?: string;
239
+ }
240
+ /**
241
+ * One background watcher a mission's agent started and has not finished.
242
+ *
243
+ * Named, because "3 monitors running" tells you nothing about whether the one
244
+ * you care about is among them. The runtime records each in the job's fan with
245
+ * its own id and the label the agent gave it.
246
+ */
247
+ export interface MissionMonitor {
248
+ missionId: string;
249
+ missionName: string;
250
+ /** The turn that owns it — the only thing that can actually be stopped. */
251
+ jobId: string;
252
+ /** The runtime's id for this watcher. */
253
+ id: string;
254
+ label: string;
255
+ startedAt: number;
256
+ /** The owning turn's latest note, for context. */
257
+ detail?: string;
258
+ }
259
+ export interface ActiveTasks {
260
+ loops: MissionLoop[];
261
+ monitors: MissionMonitor[];
262
+ }
187
263
  export interface NativeCommand {
188
264
  name: string;
189
265
  description: string;
@@ -197,6 +273,91 @@ export interface NativeCommand {
197
273
  * Only commands verified to print a text report in headless mode belong here.
198
274
  */
199
275
  export declare const NATIVE_COMMANDS: NativeCommand[];
276
+ /**
277
+ * Skills compiled into the Claude CLI itself, rather than installed under
278
+ * `.claude/skills` or a plugin cache.
279
+ *
280
+ * Skill discovery walks the filesystem, so it can only ever find skills that
281
+ * exist as files — a built-in has no `SKILL.md` anywhere on disk and is
282
+ * therefore invisible to it. Offering them means naming them.
283
+ *
284
+ * Unlike `NATIVE_COMMANDS` these are *not* intercepted: they are ordinary
285
+ * prompts that the session's own skill loader handles, so they go down the
286
+ * normal send path with their arguments intact. `matchNativeCommand` only ever
287
+ * claims a bare `/name` that appears in `NATIVE_COMMANDS`, so nothing here is
288
+ * at risk of being run out-of-band by mistake.
289
+ */
290
+ export declare const BUILTIN_SKILLS: ClaudeSkillSummary[];
291
+ /** One readable transcript belonging to a mission. */
292
+ export interface TerminalCapability {
293
+ /** Interactive writes target a live channel; follow_up starts/resumes a provider turn. */
294
+ mode: "interactive" | "follow_up" | "read_only";
295
+ connected: boolean;
296
+ acceptsInterrupt: boolean;
297
+ acceptsResize: boolean;
298
+ provider?: MissionProvider;
299
+ providerVersion?: string;
300
+ /** Detected locally but not yet promoted to Hive's production input path. */
301
+ detectedInteractiveTransport?: "codex_app_server" | "claude_stream_json";
302
+ /** Human-readable explanation for a non-interactive mode. */
303
+ reason?: string;
304
+ }
305
+ export interface TerminalStreamEvent {
306
+ streamId: string;
307
+ sequence: number;
308
+ missionId: string;
309
+ source: string;
310
+ ts: number;
311
+ method: string;
312
+ params?: Record<string, unknown>;
313
+ }
314
+ export interface TerminalInputReceipt {
315
+ clientInputId: string;
316
+ status: "accepted" | "duplicate";
317
+ streamId: string;
318
+ sequence: number;
319
+ turnId?: string;
320
+ }
321
+ export interface TranscriptSource {
322
+ /** `orchestrator`, or the worker's native agent id. */
323
+ id: string;
324
+ label: string;
325
+ kind: "orchestrator" | "worker";
326
+ sessionId: string;
327
+ /** The subagent's declared type, when the provider recorded one. */
328
+ agentType?: string;
329
+ updatedAt?: number;
330
+ bytes?: number;
331
+ terminal?: TerminalCapability;
332
+ }
333
+ export type TranscriptEntryKind =
334
+ /** Something a person (or the orchestrator) actually asked for. */
335
+ "prompt"
336
+ /** Instructions the runtime injected into the turn: a skill, a reminder. */
337
+ | "injected" | "assistant" | "thinking" | "tool_use" | "tool_result";
338
+ /** One line of the transcript, shaped the way a terminal prints it. */
339
+ export interface TranscriptEntry {
340
+ kind: TranscriptEntryKind;
341
+ ts?: number;
342
+ /** Prose, for every kind but a tool call and its result. */
343
+ text?: string;
344
+ /** Tool name, for `tool_use`. */
345
+ tool?: string;
346
+ /** `Bash(npm test)` — the tool plus the one field that says what it is doing. */
347
+ invocation?: string;
348
+ /** The tool call's full input, pretty-printed, for the expanded view. */
349
+ input?: string;
350
+ /** The tool result's body, for `tool_result`. */
351
+ output?: string;
352
+ /** Lines in the untruncated output, so the UI can say what it collapsed. */
353
+ lines?: number;
354
+ isError?: boolean;
355
+ /** Pairs a result with the call it answers. */
356
+ toolUseId?: string;
357
+ /** The skill the runtime attributed this entry to, when it recorded one. */
358
+ skill?: string;
359
+ truncated?: boolean;
360
+ }
200
361
  export type MissionLifecycleStatus = "active" | "ready_for_review" | "paused" | "completed" | "failed" | "archived";
201
362
  export interface MissionMessagesPage {
202
363
  messages: MissionMessage[];
@@ -322,7 +483,7 @@ export interface HiveEvent {
322
483
  */
323
484
  backgrounded?: boolean;
324
485
  /** For source: 'custom' — one of the orchestrator lifecycle phases. */
325
- phase?: "delegating" | "worker_started" | "worker_reported" | "reviewing" | "blocked_on_user" | "ready_for_review" | "plan_updated" | "decision_resolved" | "resuming" | "custom";
486
+ phase?: "delegating" | "worker_started" | "worker_reported" | "reviewing" | "blocked_on_user" | "ready_for_review" | "plan_updated" | "phase_started" | "phase_completed" | "task_started" | "task_completed" | "gate_satisfied" | "gate_failed" | "gate_waived" | "decision_resolved" | "resuming" | "custom";
326
487
  detail: string;
327
488
  /** Best-effort display name for the attributed worker. */
328
489
  targetWorker?: string;
@@ -12,3 +12,30 @@ export const NATIVE_COMMANDS = [
12
12
  { name: "cost", description: "Show the cost and duration breakdown for Claude usage", scope: "account" },
13
13
  { name: "context", description: "Show this mission session's context window usage", scope: "session" },
14
14
  ];
15
+ /**
16
+ * Skills compiled into the Claude CLI itself, rather than installed under
17
+ * `.claude/skills` or a plugin cache.
18
+ *
19
+ * Skill discovery walks the filesystem, so it can only ever find skills that
20
+ * exist as files — a built-in has no `SKILL.md` anywhere on disk and is
21
+ * therefore invisible to it. Offering them means naming them.
22
+ *
23
+ * Unlike `NATIVE_COMMANDS` these are *not* intercepted: they are ordinary
24
+ * prompts that the session's own skill loader handles, so they go down the
25
+ * normal send path with their arguments intact. `matchNativeCommand` only ever
26
+ * claims a bare `/name` that appears in `NATIVE_COMMANDS`, so nothing here is
27
+ * at risk of being run out-of-band by mistake.
28
+ */
29
+ export const BUILTIN_SKILLS = [
30
+ {
31
+ name: "loop",
32
+ description: "Re-send a prompt to this mission on a schedule Hive owns, so it can be listed and stopped",
33
+ argumentHint: "[interval] <prompt> · list · stop [id]",
34
+ source: "built-in",
35
+ },
36
+ {
37
+ name: "tasks",
38
+ description: "Show every loop and background monitor running, here and across the fleet",
39
+ source: "built-in",
40
+ },
41
+ ];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shanesaravia/hive",
3
3
  "private": false,
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "description": "Provider-neutral local mission control for Claude Code, Codex, and agent fleets.",
7
7
  "license": "MIT",
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { requireWorkingDirectory } from "../paths/pathResolver.js";
5
+ /**
6
+ * An agent declares its own name in frontmatter; the filename is only a
7
+ * fallback, because the two are allowed to disagree and the CLI resolves a
8
+ * mention against the declared name.
9
+ */
10
+ function metadata(file, fallback) {
11
+ let text = "";
12
+ try {
13
+ text = fs.readFileSync(file, "utf8").slice(0, 8192);
14
+ }
15
+ catch {
16
+ return { name: fallback };
17
+ }
18
+ const frontmatter = text.match(/^---\s*\n([\s\S]*?)\n---/);
19
+ const field = (name) => frontmatter?.[1].match(new RegExp(`^${name}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^['"]|['"]$/g, "");
20
+ return { name: field("name") || fallback, description: field("description") };
21
+ }
22
+ function collect(root, source) {
23
+ if (!fs.existsSync(root))
24
+ return [];
25
+ let entries;
26
+ try {
27
+ entries = fs.readdirSync(root, { withFileTypes: true });
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ return entries.flatMap((entry) => {
33
+ if (entry.name.startsWith(".") || !entry.isFile() || !entry.name.endsWith(".md"))
34
+ return [];
35
+ return [{ ...metadata(path.join(root, entry.name), path.basename(entry.name, ".md")), source }];
36
+ });
37
+ }
38
+ /**
39
+ * Project agents shadow user agents of the same name, matching how the CLI
40
+ * resolves them — the more specific definition is the one a mention will hit,
41
+ * so it is the one the composer should offer.
42
+ *
43
+ * A missing or unusable working directory is not an error here. A mission
44
+ * whose worktree has been reclaimed still resolves `@reviewer` against the
45
+ * user's own agents, and failing the whole lookup left the composer with
46
+ * nothing to complete at all — while `/` kept working, because its list has
47
+ * built-in entries that never depended on discovery.
48
+ */
49
+ export function discoverAgents(cwd) {
50
+ const claudeConfig = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
51
+ let projectAgents = [];
52
+ try {
53
+ projectAgents = collect(path.join(requireWorkingDirectory(cwd), ".claude", "agents"), "project");
54
+ }
55
+ catch { /* no usable project directory; the user's own agents still apply */ }
56
+ const all = [
57
+ ...collect(path.join(claudeConfig, "agents"), "user"),
58
+ ...projectAgents,
59
+ ];
60
+ const unique = new Map();
61
+ for (const agent of all)
62
+ unique.set(agent.name, agent);
63
+ return [...unique.values()].sort((a, b) => a.name.localeCompare(b.name));
64
+ }