@cmgmyr/hive 1.9.1 → 1.10.1

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 CHANGED
@@ -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 |
@@ -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,139 @@
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
+ if (state !== "blocked" && w.activity?.label) head.push(w.activity.label);
70
+ }
71
+ const tail = [ago(w.age_seconds)];
72
+ if (w.commits_ahead !== null && w.commits_ahead !== undefined) tail.push(`+${w.commits_ahead} commits`);
73
+ const amber = Boolean(fill) && checkpoint !== null && checkpoint !== undefined && fill.used_percent >= checkpoint;
74
+ return rowOf({
75
+ key: `w${w.id}`,
76
+ color,
77
+ id: todo ? String(todo.id) : "--",
78
+ slug: linkLabel,
79
+ parts: [
80
+ { text: `${head.join(" · ")} · ` },
81
+ { text: fill ? `ctx ${Math.round(fill.used_percent)}%` : "ctx ?", amber },
82
+ { text: ` · ${tail.join(" · ")}` },
83
+ ],
84
+ });
85
+ }
86
+
87
+ function nextLine(next, nowMs) {
88
+ if (!next) return "next: none";
89
+ const label = next.generated ? "crew notice" : next.label;
90
+ if (next.held) return `held (${next.held}): ${label}`;
91
+ const due = next.due_at && Number.isFinite(Date.parse(next.due_at)) ? ` in ${ago((Date.parse(next.due_at) - nowMs) / 1000)}` : "";
92
+ return `next${due}: ${label}`;
93
+ }
94
+
95
+ function covered(snapshot) {
96
+ const ids = new Set(snapshot.wakes.watched_worker_ids ?? []);
97
+ return snapshot.lanes.some((l) => l.worker && ids.has(l.worker.id));
98
+ }
99
+
100
+ function watchText(w) {
101
+ const parts = [w.scope === "project" ? "all workers" : w.scope || w.label, "standing"];
102
+ const until = w.max_wait_at ? new Date(w.max_wait_at) : null;
103
+ if (until && Number.isFinite(until.getTime())) {
104
+ parts.push(`until ${String(until.getHours()).padStart(2, "0")}:${String(until.getMinutes()).padStart(2, "0")}`);
105
+ }
106
+ return parts.join(" · ");
107
+ }
108
+
109
+ function rowOf(row) {
110
+ return { ...row, detail: row.parts.map((p) => p.text).join("") };
111
+ }
112
+
113
+ export function buildCrewView(snapshot, previousView, nowMs) {
114
+ const memory = previousView?.memory ?? {};
115
+ const nextMemory = {};
116
+ const rows = snapshot.lanes.map((lane) =>
117
+ lane.worker
118
+ ? workerRow(lane, snapshot, nowMs, memory, nextMemory)
119
+ : rowOf({ key: `t${lane.todo.id}`, color: "gray", id: String(lane.todo.id), slug: lane.todo.slug, parts: [{ text: "unstaffed" }] }),
120
+ );
121
+ const { wakes } = snapshot;
122
+ const workers = snapshot.lanes.filter((l) => l.worker).length;
123
+ const footer = [{ text: nextLine(wakes.next, nowMs) }];
124
+ if (wakes.watching.length > 0) footer.push({ text: `watching: ${wakes.watching.map(watchText).join(", ")}` });
125
+ else if (workers > 0 && !covered(snapshot)) footer.push({ text: "unwatched: no standing watch on running workers", amber: true });
126
+ return {
127
+ header: [
128
+ `${snapshot.project.name} · ${workers} worker${workers === 1 ? "" : "s"}`,
129
+ snapshot.needs_you.length > 0 ? `${snapshot.needs_you.length} todo${snapshot.needs_you.length === 1 ? "" : "s"} for you` : "",
130
+ ].filter(Boolean).join(" · "),
131
+ rows,
132
+ needsLabel: snapshot.needs_you.length > 0 ? `todos waiting on you (${snapshot.needs_you.length})` : "todos waiting on you",
133
+ needsYou: snapshot.needs_you.map((t) => ({ id: String(t.id), slug: t.slug })),
134
+ footer,
135
+ error: "",
136
+ memory: nextMemory,
137
+ noData: false,
138
+ };
139
+ }
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":"1.9.1","sha":"2056b89","dirty":false,"build_id":"6b9bbe7ee1cda1fc2700518664c980aac9692d27caead18970553a3f6b3cbae9"}
1
+ {"version":"1.10.1","sha":"47de74a","dirty":false,"build_id":"8d90ac219e940dac865c9770e28e1686dbed96d3d7ec219884aa4b6aa328dab5"}
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, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isLeadRowClosedHold, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, wasHeldForPaneIdentity, } from "./scheduler.js";
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 { describeVisibility, processCounts } from "./dashboard.js";
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,6 +753,8 @@ 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
 
756
+ # lead_sidebar: true # a crew pane in a claude lead (default: false; restart the lead)
757
+
748
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
@@ -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
- ? LEAD_OWNERSHIP_LABEL[rowOwnership(a, statusSnapshot())]
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)
@@ -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;