@cmgmyr/hive 1.9.0 → 1.10.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.
- package/README.md +3 -2
- package/claude-plugin/README.md +4 -0
- package/claude-plugin/crew/.claude-plugin/plugin.json +1 -0
- package/claude-plugin/crew/hooks/hooks.json +1 -0
- package/claude-plugin/crew/hooks/register.tsx +82 -0
- package/claude-plugin/crew/model.mjs +138 -0
- package/claude-plugin/crew/types/index.d.ts +18 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +55 -23
- package/dist/crew.js +507 -0
- package/dist/dashboard.js +179 -156
- package/dist/db.js +14 -0
- package/dist/harnesses.js +2 -2
- package/dist/heldLabel.js +17 -0
- package/dist/help.js +7 -3
- package/dist/leadMessage.js +29 -8
- package/dist/leadMessageDelivery.js +379 -0
- package/dist/leadState.js +8 -1
- package/dist/portfolio.js +8 -5
- package/dist/projectYml.js +14 -0
- package/dist/queenDashboard.js +30 -13
- package/dist/scheduler.js +160 -37
- package/dist/spawn.js +3 -3
- package/dist/tmux.js +42 -7
- package/dist/tools/agents.js +89 -6
- package/dist/tools/wakes.js +23 -3
- package/dist/transcript.js +135 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Run a crew of Claude Code and Codex workers from one lead session: visible tmux
|
|
|
5
5
|
|
|
6
6
|
- Visible workers: each one is a real tmux pane you can read and type into.
|
|
7
7
|
- Exact state: workers report it through their own CLI's hooks, so nothing polls.
|
|
8
|
-
- One local SQLite store: no daemon, and
|
|
8
|
+
- One local SQLite store: no daemon. The MCP server, hooks, and scheduler make no outbound requests; the CLI contacts npm only to check for updates, and upgrades can download ([details](docs/security.md)).
|
|
9
9
|
|
|
10
10
|

|
|
11
11
|
|
|
@@ -32,7 +32,7 @@ Run `cd ~/Code/your-project && hive`. It is shorthand for `hive lead`, and it op
|
|
|
32
32
|
|
|
33
33
|
## How it works
|
|
34
34
|
|
|
35
|
-
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
|
|
35
|
+
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. The MCP server, hooks, and scheduler make no outbound requests; the CLI contacts npm only to check for updates, and upgrades can download ([details](docs/security.md)). State is scoped to a project (a directory), resolved from the working directory; a lead spawns workers into tmux panes locked to that project.
|
|
36
36
|
|
|
37
37
|
## Why not subagents?
|
|
38
38
|
|
|
@@ -66,6 +66,7 @@ hive is a personal daily-driver tool, released low-key. It is single-user by des
|
|
|
66
66
|
| [Configuration](docs/configuration.md) | The `HIVE_*` environment variables |
|
|
67
67
|
| [Profiles](docs/profiles.md) | Standing instructions across projects, the session-start plugin |
|
|
68
68
|
| [Projects](docs/projects.md) | `hive init`, `hive.yml`, automatic backups, pads and todos from the shell |
|
|
69
|
+
| [Crew sidebar](docs/crew.md) | `lead_sidebar`: a crew pane in a Claude lead, and the read-only `hive crew --json` behind it |
|
|
69
70
|
| [Dashboard](docs/dashboard.md) | The generated dashboard: enabling it, where it lives, what it shows |
|
|
70
71
|
| [Install details](docs/install.md) | The interpreter pin, iTerm settings, the status line, MCP scope, codex workers, updating, uninstalling |
|
|
71
72
|
| [Troubleshooting](docs/troubleshooting.md) | Common errors and their fixes |
|
package/claude-plugin/README.md
CHANGED
|
@@ -48,3 +48,7 @@ though the kernel would have resolved that same path fine.
|
|
|
48
48
|
resolves normally; node then realpaths the main entry, which puts the shim's
|
|
49
49
|
own `../dist/kickoff.js` import inside the real checkout. No absolute machine
|
|
50
50
|
path is ever written into a committed file.
|
|
51
|
+
|
|
52
|
+
## The crew mod
|
|
53
|
+
|
|
54
|
+
`crew/` is a second, separate plugin (`hive-crew`) that `hive lead` loads with `--plugin-dir` only when `lead_sidebar: true` is set and the lead is Claude. It is not part of the symlink install above and workers never load it. See [docs/crew.md](../docs/crew.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "hive-crew", "version": "0.1.0", "description": "A docked crew pane for a hive lead, read from `hive crew --json`" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "modules": ["./register.tsx"] }
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { atom, read, update } from 'claude-code'
|
|
2
|
+
import type { EngineInterface, Register } from 'claude-code'
|
|
3
|
+
|
|
4
|
+
import { REFRESH_MS, buildCrewView, failedView, readCrew } from '../model.mjs'
|
|
5
|
+
import type { CrewView } from '../types'
|
|
6
|
+
|
|
7
|
+
const PANE = 'hive-crew'
|
|
8
|
+
const crew = atom({ plugin: 'hive-crew', key: 'crew' } as const, null as CrewView | null)
|
|
9
|
+
|
|
10
|
+
let polling = false
|
|
11
|
+
|
|
12
|
+
async function startPolling($: EngineInterface) {
|
|
13
|
+
if (polling) return
|
|
14
|
+
polling = true
|
|
15
|
+
const cwd = await $.session.cwd()
|
|
16
|
+
let busy = false
|
|
17
|
+
const refresh = async () => {
|
|
18
|
+
if (busy) return
|
|
19
|
+
busy = true
|
|
20
|
+
try {
|
|
21
|
+
const snapshot = await readCrew((argv, init) => $.process.run(argv, init), cwd)
|
|
22
|
+
await update($, crew, prev => buildCrewView(snapshot, prev, Date.now()))
|
|
23
|
+
} catch (err) {
|
|
24
|
+
await update($, crew, prev => failedView(prev, String(err instanceof Error ? err.message : err)))
|
|
25
|
+
} finally {
|
|
26
|
+
busy = false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
await refresh()
|
|
30
|
+
$.clock.every(REFRESH_MS, refresh)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const register: Register = on => {
|
|
34
|
+
on('session.start', async ($, e, next) => {
|
|
35
|
+
const { stdout } = await $.process.run(['sh', '-c', 'printf %s "$HIVE_LEAD"'])
|
|
36
|
+
if (stdout !== '1') return next(e)
|
|
37
|
+
await $.command.register({ name: 'hive-crew', description: "Show this hive project's crew in a side pane" })
|
|
38
|
+
await startPolling($)
|
|
39
|
+
const now = await read($, crew)
|
|
40
|
+
if (now && !now.error) void $.ui.open({ id: PANE, title: 'hive crew' })
|
|
41
|
+
return next(e)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
on('command.run', { command: 'hive-crew' }, async $ => {
|
|
45
|
+
await startPolling($)
|
|
46
|
+
await $.ui.open({ id: PANE, title: 'hive crew' })
|
|
47
|
+
return { text: 'hive crew pane opened.' }
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
51
|
+
const { Box, Text } = $.ui.resolve(e)
|
|
52
|
+
const c = await read($, crew)
|
|
53
|
+
if (!c) return <Text dimColor>reading the hive crew…</Text>
|
|
54
|
+
if (c.noData) {
|
|
55
|
+
return (
|
|
56
|
+
<Box flexDirection="column">
|
|
57
|
+
<Text color="red" wrap="truncate-end">read failed: {c.error}</Text>
|
|
58
|
+
<Text dimColor>no data yet</Text>
|
|
59
|
+
</Box>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
<Box flexDirection="column">
|
|
64
|
+
<Text bold wrap="truncate-end">{c.header}</Text>
|
|
65
|
+
{c.error && <Text color="red" wrap="truncate-end">read failed: {c.error}</Text>}
|
|
66
|
+
{c.rows.length === 0 && <Text dimColor> no lanes</Text>}
|
|
67
|
+
{c.rows.map(r => (
|
|
68
|
+
<Box flexDirection="column">
|
|
69
|
+
<Text wrap="truncate-end"><Text color={r.color}>● </Text>{r.id} <Text dimColor>{r.slug}</Text></Text>
|
|
70
|
+
<Text wrap="truncate-end" dimColor> {r.parts.map(p => <Text color={p.amber ? 'yellow' : undefined}>{p.text}</Text>)}</Text>
|
|
71
|
+
</Box>
|
|
72
|
+
))}
|
|
73
|
+
<Text> </Text>
|
|
74
|
+
<Text bold color={c.needsYou.length ? 'red' : undefined}>{c.needsLabel}</Text>
|
|
75
|
+
{c.needsYou.length === 0 && <Text dimColor> none</Text>}
|
|
76
|
+
{c.needsYou.map(t => <Text wrap="truncate-end"> {t.id} <Text dimColor>{t.slug}</Text></Text>)}
|
|
77
|
+
<Text> </Text>
|
|
78
|
+
{c.footer.map(line => <Text dimColor={!line.amber} color={line.amber ? 'yellow' : undefined} wrap="truncate-end">{line.text}</Text>)}
|
|
79
|
+
</Box>
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export const SCHEMA_VERSION = 1;
|
|
2
|
+
export const REFRESH_MS = 5000;
|
|
3
|
+
export const TIMEOUT_MS = 4000;
|
|
4
|
+
|
|
5
|
+
export function ago(seconds) {
|
|
6
|
+
const s = Math.max(0, Math.floor(seconds));
|
|
7
|
+
if (s < 60) return `${s}s`;
|
|
8
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
9
|
+
return `${Math.floor(s / 3600)}h${Math.floor((s % 3600) / 60)}m`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function readCrew(run, cwd) {
|
|
13
|
+
let res;
|
|
14
|
+
try {
|
|
15
|
+
res = await run(["hive", "crew", "--json"], { cwd, timeoutMs: TIMEOUT_MS });
|
|
16
|
+
} catch (err) {
|
|
17
|
+
throw new Error(`hive crew did not answer: ${String(err instanceof Error ? err.message : err).slice(0, 100)}`);
|
|
18
|
+
}
|
|
19
|
+
if (res.exitCode !== 0) {
|
|
20
|
+
const line = String(res.stderr ?? "").split("\n").map((l) => l.trim()).find((l) => l !== "");
|
|
21
|
+
throw new Error(`exit ${res.exitCode}${line ? `: ${line.slice(0, 120)}` : ""}`);
|
|
22
|
+
}
|
|
23
|
+
let snap;
|
|
24
|
+
try {
|
|
25
|
+
snap = JSON.parse(res.stdout);
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error("hive crew returned invalid JSON");
|
|
28
|
+
}
|
|
29
|
+
if (snap === null || typeof snap !== "object" || snap.schema_version !== SCHEMA_VERSION) {
|
|
30
|
+
throw new Error(`hive crew schema ${snap?.schema_version ?? "unknown"} is not ${SCHEMA_VERSION}; update hive`);
|
|
31
|
+
}
|
|
32
|
+
if (!Array.isArray(snap.lanes) || !Array.isArray(snap.needs_you) || snap.wakes === null || typeof snap.wakes !== "object" || !Array.isArray(snap.wakes.watching)) {
|
|
33
|
+
throw new Error("hive crew JSON is missing lanes, needs_you or wakes");
|
|
34
|
+
}
|
|
35
|
+
return snap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function emptyView() {
|
|
39
|
+
return { header: "", rows: [], needsLabel: "", needsYou: [], footer: [], error: "", memory: {}, noData: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function failedView(previousView, message) {
|
|
43
|
+
return { ...(previousView ?? emptyView()), error: message };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function duration(worker, activity, nowMs, memory, nextMemory) {
|
|
47
|
+
const key = `${worker.id}|${worker.session_id}|${activity.label}`;
|
|
48
|
+
const stamp = activity.since;
|
|
49
|
+
const at = stamp ? Date.parse(stamp) : NaN;
|
|
50
|
+
if (Number.isFinite(at)) return `${activity.lower_bound ? ">=" : ""}${ago((nowMs - at) / 1000)}`;
|
|
51
|
+
const first = memory[key] ?? nowMs;
|
|
52
|
+
nextMemory[key] = first;
|
|
53
|
+
return `~${ago((nowMs - first) / 1000)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function workerRow(lane, snapshot, nowMs, memory, nextMemory) {
|
|
57
|
+
const w = lane.worker;
|
|
58
|
+
const checkpoint = snapshot.context_checkpoint_percent;
|
|
59
|
+
const todo = lane.todo;
|
|
60
|
+
const linkLabel = todo ? todo.slug : lane.pad ? `pad ${lane.pad}` : `${w.name} unlinked`;
|
|
61
|
+
const state = w.state === "blocked" ? "blocked" : w.your_turn ? "your turn" : w.state;
|
|
62
|
+
const color = state === "blocked" ? "red" : state === "your turn" ? "yellow" : state === "working" ? "green" : "gray";
|
|
63
|
+
const fill = w.context_fill;
|
|
64
|
+
const head = [w.model || w.harness];
|
|
65
|
+
if (state === "working") head.push(`${w.activity.label} ${duration(w, w.activity, nowMs, memory, nextMemory)}`);
|
|
66
|
+
else {
|
|
67
|
+
const since = state === "blocked" ? null : w.state_changed_at;
|
|
68
|
+
head.push(`${state} ${duration(w, { label: state, since, lower_bound: false }, nowMs, memory, nextMemory)}`);
|
|
69
|
+
}
|
|
70
|
+
const tail = [ago(w.age_seconds)];
|
|
71
|
+
if (w.commits_ahead !== null && w.commits_ahead !== undefined) tail.push(`+${w.commits_ahead} commits`);
|
|
72
|
+
const amber = Boolean(fill) && checkpoint !== null && checkpoint !== undefined && fill.used_percent >= checkpoint;
|
|
73
|
+
return rowOf({
|
|
74
|
+
key: `w${w.id}`,
|
|
75
|
+
color,
|
|
76
|
+
id: todo ? String(todo.id) : "--",
|
|
77
|
+
slug: linkLabel,
|
|
78
|
+
parts: [
|
|
79
|
+
{ text: `${head.join(" · ")} · ` },
|
|
80
|
+
{ text: fill ? `ctx ${Math.round(fill.used_percent)}%` : "ctx ?", amber },
|
|
81
|
+
{ text: ` · ${tail.join(" · ")}` },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function nextLine(next, nowMs) {
|
|
87
|
+
if (!next) return "next: none";
|
|
88
|
+
const label = next.generated ? "crew notice" : next.label;
|
|
89
|
+
if (next.held) return `held (${next.held}): ${label}`;
|
|
90
|
+
const due = next.due_at && Number.isFinite(Date.parse(next.due_at)) ? ` in ${ago((Date.parse(next.due_at) - nowMs) / 1000)}` : "";
|
|
91
|
+
return `next${due}: ${label}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function covered(snapshot) {
|
|
95
|
+
const ids = new Set(snapshot.wakes.watched_worker_ids ?? []);
|
|
96
|
+
return snapshot.lanes.some((l) => l.worker && ids.has(l.worker.id));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function watchText(w) {
|
|
100
|
+
const parts = [w.scope === "project" ? "all workers" : w.scope || w.label, "standing"];
|
|
101
|
+
const until = w.max_wait_at ? new Date(w.max_wait_at) : null;
|
|
102
|
+
if (until && Number.isFinite(until.getTime())) {
|
|
103
|
+
parts.push(`until ${String(until.getHours()).padStart(2, "0")}:${String(until.getMinutes()).padStart(2, "0")}`);
|
|
104
|
+
}
|
|
105
|
+
return parts.join(" · ");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function rowOf(row) {
|
|
109
|
+
return { ...row, detail: row.parts.map((p) => p.text).join("") };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildCrewView(snapshot, previousView, nowMs) {
|
|
113
|
+
const memory = previousView?.memory ?? {};
|
|
114
|
+
const nextMemory = {};
|
|
115
|
+
const rows = snapshot.lanes.map((lane) =>
|
|
116
|
+
lane.worker
|
|
117
|
+
? workerRow(lane, snapshot, nowMs, memory, nextMemory)
|
|
118
|
+
: rowOf({ key: `t${lane.todo.id}`, color: "gray", id: String(lane.todo.id), slug: lane.todo.slug, parts: [{ text: "unstaffed" }] }),
|
|
119
|
+
);
|
|
120
|
+
const { wakes } = snapshot;
|
|
121
|
+
const workers = snapshot.lanes.filter((l) => l.worker).length;
|
|
122
|
+
const footer = [{ text: nextLine(wakes.next, nowMs) }];
|
|
123
|
+
if (wakes.watching.length > 0) footer.push({ text: `watching: ${wakes.watching.map(watchText).join(", ")}` });
|
|
124
|
+
else if (workers > 0 && !covered(snapshot)) footer.push({ text: "unwatched: no standing watch on running workers", amber: true });
|
|
125
|
+
return {
|
|
126
|
+
header: [
|
|
127
|
+
`${snapshot.project.name} · ${workers} worker${workers === 1 ? "" : "s"}`,
|
|
128
|
+
snapshot.needs_you.length > 0 ? `${snapshot.needs_you.length} todo${snapshot.needs_you.length === 1 ? "" : "s"} for you` : "",
|
|
129
|
+
].filter(Boolean).join(" · "),
|
|
130
|
+
rows,
|
|
131
|
+
needsLabel: snapshot.needs_you.length > 0 ? `todos waiting on you (${snapshot.needs_you.length})` : "todos waiting on you",
|
|
132
|
+
needsYou: snapshot.needs_you.map((t) => ({ id: String(t.id), slug: t.slug })),
|
|
133
|
+
footer,
|
|
134
|
+
error: "",
|
|
135
|
+
memory: nextMemory,
|
|
136
|
+
noData: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Part = { text: string; amber?: boolean }
|
|
2
|
+
export type Row = { key: string; color: string; id: string; slug: string; detail: string; parts: Part[] }
|
|
3
|
+
export type CrewView = {
|
|
4
|
+
header: string
|
|
5
|
+
rows: Row[]
|
|
6
|
+
needsLabel: string
|
|
7
|
+
needsYou: { id: string; slug: string }[]
|
|
8
|
+
footer: { text: string; amber?: boolean }[]
|
|
9
|
+
error: string
|
|
10
|
+
memory: Record<string, number>
|
|
11
|
+
noData: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module 'claude-code' {
|
|
15
|
+
interface PluginState {
|
|
16
|
+
'hive-crew': { crew: CrewView | null }
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.
|
|
1
|
+
{"version":"1.10.0","sha":"9dd5fee","dirty":false,"build_id":"d7050d60f67cf44e2ca833e2ba422563b4e6b8fbdd68bccd51772e429cac1fa5"}
|
package/dist/cli.js
CHANGED
|
@@ -22,8 +22,10 @@ import { confirmQueenWrite, listQueenAudit } from "./queenAudit.js";
|
|
|
22
22
|
import { formatRowCounts, projectRowCounts, removeProject } from "./projectRemove.js";
|
|
23
23
|
import { ensureHooksFile, ensureLeadHooksFile } from "./hooks.js";
|
|
24
24
|
import { errorMessage, registrationNoticeText, withTrailingNewline } from "./result.js";
|
|
25
|
-
import { ACTIVE_TIMER_WHERE, socketAwaitingWhere, dashboardFileContained, describeStall,
|
|
25
|
+
import { ACTIVE_TIMER_WHERE, socketAwaitingWhere, dashboardFileContained, describeStall, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, } from "./scheduler.js";
|
|
26
26
|
import { readTurnCount } from "./turnCount.js";
|
|
27
|
+
import { leadSessionEnded } from "./leadState.js";
|
|
28
|
+
import { heldReasonLabel } from "./heldLabel.js";
|
|
27
29
|
import { collectPortfolio, leadText } from "./portfolio.js";
|
|
28
30
|
import { STALL_BOUND_SECONDS } from "./backgroundTasks.js";
|
|
29
31
|
import { probeSessionInterpreter, reexecTarget, sessionStartVerdict, } from "./sessionProbe.js";
|
|
@@ -31,7 +33,8 @@ import { backupHealth, backupNow, backupsDir, formatBytes, listSnapshots, previe
|
|
|
31
33
|
import { describeTeardownWindow, readTeardowns, teardownLogPath, } from "./teardown.js";
|
|
32
34
|
import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchAgent, LEAD_KIND, LEAD_NAME, mintLeadActorId, relayoutAfterPaneLeft, upsertActor, withWindowClaim, } from "./spawn.js";
|
|
33
35
|
import { adoptableWindow, applyLayout, claimInitialWindow, configureHiveWindow, controlModeFor, createWindow, DEFAULT_LAYOUT, describePaneChoice, ensureSession, findProjectWindow, foreignSocket, isViewSessionName, listOwnedWindows, liveTargets, observationFailed, observeRowOwnership, ORPHAN_MIN_AGE_MS, orphanScratchServers, orphansWorthWarningAbout, paneIsAloneInWindow, paneVisibility, panePidForRecord, PROCESSES_LAYOUT, processesPaneTitle, processesWindowName, projectWindows, RAW_ATTACH_TMUX_CONFIG, renderAttachCommand, resolveAttachTarget, resolveInTmuxTarget, rowLive, rowOwnership, SESSION_PREFIX, appendGlobalHook, globalHooks, replaceGlobalHooks, sessionName, setPaneTitle, shellQuote, shownPaneTitle, makeProcessesWindow, tmux, TMUX_DOC, tmuxSaysNothingThere, tmuxTimeoutOverride, tmuxSocketPath, untrustedTmuxServer, waitForPaneEstablished, windowLayout, } from "./tmux.js";
|
|
34
|
-
import {
|
|
36
|
+
import { collectCrew, crewStoreProblem } from "./crew.js";
|
|
37
|
+
import { describeVisibility, formatLocal, processCounts } from "./dashboard.js";
|
|
35
38
|
import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, STOP_REASONS, } from "./processes.js";
|
|
36
39
|
import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
|
|
37
40
|
import { writeProjectPosture } from "./brief.js";
|
|
@@ -67,6 +70,8 @@ Usage:
|
|
|
67
70
|
hive show <process> [path] move a running process's pane beside the lead
|
|
68
71
|
hive hide <process> [path] move it back into the <project>/processes window
|
|
69
72
|
hive status overview of agents, todos, and wake-ups everywhere
|
|
73
|
+
hive crew --json this project's workers, todos and wakes as one JSON
|
|
74
|
+
snapshot; read-only, writes nothing
|
|
70
75
|
hive portfolio [--json] one row per registered project: lane, lead, workers,
|
|
71
76
|
todos, needs-human count, wakes; read-only
|
|
72
77
|
hive queen-audit confirmed queen writes, newest first; 30-day history
|
|
@@ -450,6 +455,9 @@ async function cmdLead(argv) {
|
|
|
450
455
|
leadCommand += ` --name ${shellQuote(project.name)}`;
|
|
451
456
|
}
|
|
452
457
|
leadCommand += ` ${leadHarness.briefDelivery.settingsArgs(hooksPath).map(shellQuote).join(" ")}`;
|
|
458
|
+
if (config?.lead_sidebar === true && leadHarness.name === "claude") {
|
|
459
|
+
leadCommand += ` ${["--plugin-dir", crewPluginDir()].map(shellQuote).join(" ")}`;
|
|
460
|
+
}
|
|
453
461
|
if (renderedPosture !== null) {
|
|
454
462
|
const posturePath = writeProjectPosture(project.id, renderedPosture);
|
|
455
463
|
leadCommand += ` ${leadHarness.briefDelivery.systemPromptArgs(posturePath).map(shellQuote).join(" ")}`;
|
|
@@ -745,7 +753,9 @@ const HIVE_YML_TEMPLATE = `# hive project config. Read by \`hive lead\` from the
|
|
|
745
753
|
|
|
746
754
|
# dashboard: true # write .hive/dashboard.html (default: false)
|
|
747
755
|
|
|
748
|
-
#
|
|
756
|
+
# lead_sidebar: true # a crew pane in a claude lead (default: false; restart the lead)
|
|
757
|
+
|
|
758
|
+
# quiet_messaging: true # wakes and worker messages reach a claude lead as one row (default: false; restart the lead)
|
|
749
759
|
|
|
750
760
|
# vars: # substituted into the profile runbook
|
|
751
761
|
# repo: owner/name # {{repo}}
|
|
@@ -1278,6 +1288,9 @@ function cmdProfile(argv) {
|
|
|
1278
1288
|
throw e;
|
|
1279
1289
|
}
|
|
1280
1290
|
}
|
|
1291
|
+
function crewPluginDir() {
|
|
1292
|
+
return join(checkoutRoot, "claude-plugin", "crew");
|
|
1293
|
+
}
|
|
1281
1294
|
const DASHBOARD_OPENED_KV_KEY = "hive:dashboard_opened";
|
|
1282
1295
|
const DASHBOARD_OPENED_TTL_SECONDS = 8 * 60 * 60;
|
|
1283
1296
|
function maybeOpenDashboard(project, dashboardEnabled) {
|
|
@@ -1605,6 +1618,10 @@ const LEAD_OWNERSHIP_LABEL = {
|
|
|
1605
1618
|
reissued: "no live pane",
|
|
1606
1619
|
unknown: "pane identity unknown",
|
|
1607
1620
|
};
|
|
1621
|
+
function leadStatusLabel(row, ownership) {
|
|
1622
|
+
const ended = leadSessionEnded(row, ownership);
|
|
1623
|
+
return ended ? `dormant (session ended ${formatLocal(ended.ended_at)})` : LEAD_OWNERSHIP_LABEL[ownership];
|
|
1624
|
+
}
|
|
1608
1625
|
function cmdStatus() {
|
|
1609
1626
|
janitor();
|
|
1610
1627
|
let anyOutput = false;
|
|
@@ -1656,7 +1673,7 @@ function cmdStatus() {
|
|
|
1656
1673
|
const state = a.kind === "agent"
|
|
1657
1674
|
? describeForHuman(deriveProvenance(a, null))
|
|
1658
1675
|
: a.kind === LEAD_KIND
|
|
1659
|
-
?
|
|
1676
|
+
? leadStatusLabel(a, rowOwnership(a, statusSnapshot()))
|
|
1660
1677
|
: (whereIs.get(a.name) ?? "running");
|
|
1661
1678
|
const label = a.kind === "command" ? "cmd " : a.kind === LEAD_KIND ? "lead " : "agent";
|
|
1662
1679
|
console.log(` ${label} ${a.name.padEnd(20)} ${state}`);
|
|
@@ -2819,22 +2836,6 @@ function cmdStatusline() {
|
|
|
2819
2836
|
}
|
|
2820
2837
|
console.log(`\x1b[33m⬡\x1b[0m \x1b[2mhive:\x1b[0m ${parts.join(" \x1b[2m·\x1b[0m ")}`);
|
|
2821
2838
|
}
|
|
2822
|
-
// test/docs.test.mjs fails if a label here is missing from docs/install.md. It checks
|
|
2823
|
-
// presence only: docs/install.md also counts the set in prose, and nothing guards that number.
|
|
2824
|
-
export const HELD_REASON_LABELS = ["typing", "talking", "needs you", "blocked"];
|
|
2825
|
-
export function heldReasonLabel(heldReason) {
|
|
2826
|
-
if (isUnsubmittedInputHold(heldReason))
|
|
2827
|
-
return "typing";
|
|
2828
|
-
if (heldReason === HELD_REASON_LEAD_PANE_DEAD ||
|
|
2829
|
-
wasHeldForPaneIdentity(heldReason) ||
|
|
2830
|
-
isLeadRowClosedHold(heldReason) ||
|
|
2831
|
-
isUnclassifiablePaneHold(heldReason)) {
|
|
2832
|
-
return "needs you";
|
|
2833
|
-
}
|
|
2834
|
-
if (heldReason === HELD_REASON_CONVERSATION)
|
|
2835
|
-
return "talking";
|
|
2836
|
-
return "blocked";
|
|
2837
|
-
}
|
|
2838
2839
|
function padSlug(name) {
|
|
2839
2840
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "pad";
|
|
2840
2841
|
}
|
|
@@ -2955,6 +2956,34 @@ function cmdQueenAudit(argv) {
|
|
|
2955
2956
|
` ${entry.operation} ${entry.resource_type} #${entry.resource_id}\n ${entry.summary}`);
|
|
2956
2957
|
}
|
|
2957
2958
|
}
|
|
2959
|
+
function cmdCrew(argv) {
|
|
2960
|
+
const parsed = parseArgs(argv, { flags: ["--json"] });
|
|
2961
|
+
if (parsed.positional.length > 0)
|
|
2962
|
+
parsed.unknown.push(parsed.positional[0]);
|
|
2963
|
+
rejectUnknownFlags("crew", parsed, "--json");
|
|
2964
|
+
if (!parsed.flags.has("--json")) {
|
|
2965
|
+
console.error("hive crew: usage: hive crew --json. It prints one JSON snapshot of this project's workers, todos and wakes.");
|
|
2966
|
+
process.exit(1);
|
|
2967
|
+
}
|
|
2968
|
+
const problem = crewStoreProblem();
|
|
2969
|
+
if (problem) {
|
|
2970
|
+
console.error(`hive crew: ${problem}. Run any other hive command once (for example hive status) to migrate it, then retry.`);
|
|
2971
|
+
process.exit(1);
|
|
2972
|
+
}
|
|
2973
|
+
let project;
|
|
2974
|
+
try {
|
|
2975
|
+
project = pinnedOrCwdProject();
|
|
2976
|
+
}
|
|
2977
|
+
catch (e) {
|
|
2978
|
+
console.error(`hive crew: ${errorMessage(e)}`);
|
|
2979
|
+
process.exit(1);
|
|
2980
|
+
}
|
|
2981
|
+
if (!project) {
|
|
2982
|
+
console.error("hive crew: this directory is not a registered hive project. cd to a registered checkout, or run `hive init` here to register it.");
|
|
2983
|
+
process.exit(1);
|
|
2984
|
+
}
|
|
2985
|
+
console.log(JSON.stringify(collectCrew(project, new Date())));
|
|
2986
|
+
}
|
|
2958
2987
|
function cmdPortfolio(argv) {
|
|
2959
2988
|
const parsed = parseArgs(argv, { flags: ["--json"] });
|
|
2960
2989
|
if (parsed.positional.length > 0)
|
|
@@ -3376,7 +3405,7 @@ function cmdPad(argv) {
|
|
|
3376
3405
|
process.stdout.write(withTrailingNewline(pad.content));
|
|
3377
3406
|
}
|
|
3378
3407
|
process.stdout.on("error", (error) => {
|
|
3379
|
-
if (error.code === "EPIPE")
|
|
3408
|
+
if (error.code === "EPIPE" || error.code === "ENOTCONN")
|
|
3380
3409
|
process.exit(0);
|
|
3381
3410
|
throw error;
|
|
3382
3411
|
});
|
|
@@ -3404,7 +3433,7 @@ if (command === "--version" || command === "-v") {
|
|
|
3404
3433
|
process.exit(0);
|
|
3405
3434
|
}
|
|
3406
3435
|
const COMMANDS = [
|
|
3407
|
-
"lead", "queen", "queen-audit", "init", "attach", "start", "stop", "show", "hide", "status", "portfolio", "next", "setup", "upgrade", "doctor",
|
|
3436
|
+
"lead", "queen", "queen-audit", "init", "attach", "start", "stop", "show", "hide", "status", "portfolio", "crew", "next", "setup", "upgrade", "doctor",
|
|
3408
3437
|
LEAD_PANE_EXITED_VERB,
|
|
3409
3438
|
"pads", "pad", "todos", "todo", "backups", "restore", "project", "runbook", "posture", "profile", "kickoff", "statusline",
|
|
3410
3439
|
];
|
|
@@ -3421,7 +3450,7 @@ if (!COMMANDS.includes(command)) {
|
|
|
3421
3450
|
usage();
|
|
3422
3451
|
}
|
|
3423
3452
|
}
|
|
3424
|
-
if (command !== "upgrade")
|
|
3453
|
+
if (command !== "upgrade" && command !== "crew")
|
|
3425
3454
|
migrate();
|
|
3426
3455
|
try {
|
|
3427
3456
|
switch (command) {
|
|
@@ -3458,6 +3487,9 @@ try {
|
|
|
3458
3487
|
case "portfolio":
|
|
3459
3488
|
cmdPortfolio(rest);
|
|
3460
3489
|
break;
|
|
3490
|
+
case "crew":
|
|
3491
|
+
cmdCrew(rest);
|
|
3492
|
+
break;
|
|
3461
3493
|
case "queen-audit":
|
|
3462
3494
|
cmdQueenAudit(rest);
|
|
3463
3495
|
break;
|