@cmgmyr/hive 1.3.0 → 1.5.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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, spawn, spawnSync } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
- import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync, } from "node:fs";
4
+ import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync, } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { join, resolve, sep } from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
@@ -12,16 +12,19 @@ import { claudeConfigDir } from "./claudeDir.js";
12
12
  import { ATTACH_MODES, AUTO_ATTACH_MODES, attachMode, isAttachMode, isAutoAttach, migrateLegacyConfig, resolvedAutoAttach, setAttachMode, setAutoAttach, } from "./config.js";
13
13
  import { cliPath, dispatcherDir, dispatcherPath, dispatcherScript, durabilityLines, firstHiveOnPath, linkedWorktreePin, pathAdvice, readDispatcher, } from "./dispatcher.js";
14
14
  import { codexConfigPath, codexHiveRegistrations, codexRegistrationProblem, hiveRegistrations, registrationOffer, registrationProblem, } from "./mcpConfig.js";
15
- import { DEFAULT_DATA_DIR } from "./dataDir.js";
15
+ import { DEFAULT_DATA_DIR, storeDir } from "./dataDir.js";
16
16
  import { dataDir, db, migrate, storeSchemaAhead } from "./db.js";
17
17
  import { cacheIsStale, queryUpdate, readCachedUpdate, refreshUpdate, shouldAutoRefresh, updateLine } from "./updateCheck.js";
18
18
  import { checkoutUpgradeSteps, detectInstallShape, globalUpgradeSteps } from "./upgrade.js";
19
19
  import { isLowHeadroom, orphanLoginShellDetails, ptyHeadroom } from "./ptys.js";
20
- import { addProject, agentProjectPin, currentActor, effectiveProjectId, findProjectForCwd, getProjectByPath, gitPrimaryRoot, getProject, listProjects, registeredAncestor, takeRegistrationNotice, } from "./context.js";
20
+ import { addProject, agentProjectPin, assertQueenCrossProjectWrite, captureQueenWriteIdentity, currentActor, effectiveProjectId, findProjectForCwd, findProjectForDir, getProjectByPath, gitPrimaryRoot, getProject, listProjects, QUEEN_PROFILE, queenHomeDir, registeredAncestor, takeRegistrationNotice, } from "./context.js";
21
+ import { confirmQueenWrite, listQueenAudit } from "./queenAudit.js";
22
+ import { formatRowCounts, projectRowCounts, removeProject } from "./projectRemove.js";
21
23
  import { ensureHooksFile } from "./hooks.js";
22
24
  import { errorMessage, registrationNoticeText, withTrailingNewline } from "./result.js";
23
- import { ACTIVE_TIMER_WHERE, dashboardFileContained, describeStall, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, transcriptStaleness, wasHeldForPaneReissue, } from "./scheduler.js";
25
+ import { ACTIVE_TIMER_WHERE, dashboardFileContained, describeStall, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, wasHeldForPaneReissue, } from "./scheduler.js";
24
26
  import { readTurnCount } from "./turnCount.js";
27
+ import { collectPortfolio, leadText } from "./portfolio.js";
25
28
  import { STALL_BOUND_SECONDS } from "./backgroundTasks.js";
26
29
  import { probeSessionInterpreter, reexecTarget, sessionStartVerdict, } from "./sessionProbe.js";
27
30
  import { backupHealth, backupNow, backupsDir, formatBytes, listSnapshots, previewRestore, restoreSnapshot, totalSizeBytes, } from "./backup.js";
@@ -30,11 +33,11 @@ import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchA
30
33
  import { adoptableWindow, applyLayout, claimInitialWindow, configureHiveWindow, controlModeFor, createWindow, DEFAULT_LAYOUT, describePaneChoice, ensureSession, findProjectWindow, foreignSocket, isPaneTarget, isViewSessionName, listOwnedWindows, ORPHAN_MIN_AGE_MS, orphanScratchServers, orphansWorthWarningAbout, paneIsAloneInWindow, paneVisibility, panePid, PROCESSES_LAYOUT, processesPaneTitle, processesWindowName, projectWindows, RAW_ATTACH_TMUX_CONFIG, renderAttachCommand, resolveAttachTarget, resolveInTmuxTarget, paneReissued, rowLive, rowLiveProbe, SESSION_PREFIX, appendGlobalHook, globalHooks, replaceGlobalHooks, sessionName, setPaneTitle, shellQuote, shownPaneTitle, makeProcessesWindow, tmux, TMUX_DOC, tmuxSaysNothingThere, tmuxTimeoutOverride, tmuxSocketPath, untrustedTmuxServer, waitForPaneEstablished, windowLayout, } from "./tmux.js";
31
34
  import { describeVisibility, processCounts } from "./dashboard.js";
32
35
  import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, STOP_REASONS, } from "./processes.js";
33
- import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, } from "./projectYml.js";
36
+ import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
34
37
  import { writeProjectPosture } from "./brief.js";
35
38
  import { carriesNameFlag, harnessFor, hasTranscriptSignal, paneClassifierFor, transcriptDirFor } from "./harnesses.js";
36
39
  import { codexHomeDir, codexInstructionsPhrase, ensureCodexHome, reapCodexHome } from "./codexHome.js";
37
- import { TRIAGE_MESSAGE } from "./kickoff.js";
40
+ import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
38
41
  import { ageSecondsSince, deriveProvenance, describeForHuman, describeLastLogEvent, humanizeAge, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "./stateProvenance.js";
39
42
  import { awaitingFirstPromptSql } from "./firstPrompt.js";
40
43
  import { checkoutRoot, createProfile, forkProfile, PROFILE_FILES, ProfileError, profileExists, profileFileNames, profileNames, profileStatus, readProfileFile, referencedPads, referencedPaths, renderProfileFile, resolveProfileFile, REWRITE_THRESHOLD, templateVars, userProfilesDir, } from "./profiles.js";
@@ -46,11 +49,16 @@ function usage() {
46
49
  Usage:
47
50
  hive --version version, short sha, and dirty marker for this build
48
51
  hive [path] open the project's session with a lead window
49
- hive lead [path] [--no-dashboard]
52
+ hive lead [path] [--no-dashboard] [--detach]
50
53
  same; lead is the default command. --no-dashboard
51
54
  skips this run's dashboard auto-open -
52
55
  scripts/restart-lead.sh passes it; a human rarely
53
- needs to
56
+ needs it. --detach starts or adopts the lead and
57
+ prints its pane and attach command without attaching
58
+ hive queen [--no-dashboard]
59
+ start, or reattach to, the lead that reads across
60
+ every registered project; its home is
61
+ <data dir>/queen, created on first run
54
62
  hive init [path] [--profile <name>|--no-profile]
55
63
  set the project up: hive.yml, profile, starter pads
56
64
  hive attach [path] attach without adding windows
@@ -59,6 +67,14 @@ Usage:
59
67
  hive show <process> [path] move a running process's pane beside the lead
60
68
  hive hide <process> [path] move it back into the <project>/processes window
61
69
  hive status overview of agents, todos, and wake-ups everywhere
70
+ hive portfolio [--json] one row per registered project: lane, lead, workers,
71
+ todos, needs-human count, wakes; read-only
72
+ hive queen-audit confirmed queen writes, newest first; 30-day history
73
+ [--project-id <id>] filter the target project (queen only across projects)
74
+ [--limit <n>] [--json] default 20, maximum 100; JSON prints one object
75
+ hive next [--print] attach to the lead of the project that needs you most
76
+ (waiting on you, then stuck), starting a dead or
77
+ missing lead first; --print only names the choice
62
78
  hive upgrade [--check] upgrade a global npm install; preview with --check
63
79
  hive upgrade --run run the printed pull/install/build/setup checkout recipe
64
80
  hive setup [--dir <dir>] write a \`hive\` that runs the interpreter this build
@@ -80,6 +96,9 @@ Usage:
80
96
  hive todo <id> print one todo in full, comments included
81
97
  hive backups list automatic store snapshots
82
98
  hive restore <name> [--yes] [--force] overwrite the live store from a snapshot
99
+ hive project rm <id|path> [--yes]
100
+ remove another project and everything it owns,
101
+ after a snapshot; never the one you are in
83
102
  hive runbook this project's standing process, vars resolved
84
103
  hive posture the posture text this project's lead starts with
85
104
  hive kickoff [--explain] SessionStart hook output; silent unless this is a lead checkout
@@ -124,6 +143,27 @@ function resolveProject(path, onNotice) {
124
143
  const pinnedProject = getProject(pinned);
125
144
  throw new Error(`This session is locked to project ${pinned} ("${pinnedProject.name}") but "${path}" resolves to ${target ? `project ${target.id} ("${target.name}")` : "no registered project"}. An explicit path argument cannot escape HIVE_PROJECT_LOCK=1. Unset HIVE_AGENT_ID and HIVE_PROJECT_LOCK in this pane, or open a new one, to act on a different project.`);
126
145
  }
146
+ function resolveProjectForWrite(verb, path, onNotice) {
147
+ assertQueenCrossProjectWrite(verb, findProjectForDir(path ?? process.cwd())?.id ?? null, {});
148
+ return resolveProject(path, onNotice);
149
+ }
150
+ function resolveProjectForRead(verb, path) {
151
+ if (path) {
152
+ process.chdir(path);
153
+ const pinned = agentProjectPin();
154
+ if (pinned != null) {
155
+ const target = findProjectForCwd();
156
+ if (target == null || target.id !== pinned) {
157
+ throw new Error(`This session is locked to project ${pinned} ("${getProject(pinned).name}") but "${path}" resolves to ${target ? `project ${target.id} ("${target.name}")` : "no registered project"}. An explicit path argument cannot escape HIVE_PROJECT_LOCK=1. Unset HIVE_AGENT_ID and HIVE_PROJECT_LOCK in this pane, or open a new one, to act on a different project.`);
158
+ }
159
+ }
160
+ }
161
+ const project = pinnedOrCwdProject();
162
+ if (project)
163
+ return project;
164
+ console.error(`hive ${verb}: this directory is not a registered hive project. cd to a registered checkout, or run \`hive init\` here to register it.`);
165
+ process.exit(1);
166
+ }
127
167
  function pinnedOrCwdProject() {
128
168
  const pinned = agentProjectPin();
129
169
  if (pinned != null)
@@ -139,12 +179,9 @@ async function confirmYesNo(question) {
139
179
  return /^y(es)?$/i.test(answer.trim());
140
180
  }
141
181
  async function ensureTrusted(projectId, name, command, dir, env) {
142
- const hash = configHash(name, command, dir, env);
143
- const trusted = db
144
- .prepare("SELECT 1 FROM command_trust WHERE project_id = ? AND name = ? AND config_hash = ?")
145
- .get(projectId, name, hash);
146
- if (trusted)
182
+ if (isTrusted(projectId, name, command, dir, env))
147
183
  return true;
184
+ const hash = configHash(name, command, dir, env);
148
185
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
149
186
  console.log(`! "${name}" is not trusted yet; run hive interactively to review it.`);
150
187
  return false;
@@ -162,6 +199,12 @@ async function ensureTrusted(projectId, name, command, dir, env) {
162
199
  db.prepare("INSERT OR IGNORE INTO command_trust (project_id, name, config_hash) VALUES (?, ?, ?)").run(projectId, name, hash);
163
200
  return true;
164
201
  }
202
+ function isTrusted(projectId, name, command, dir, env) {
203
+ const hash = configHash(name, command, dir, env);
204
+ return !!db
205
+ .prepare("SELECT 1 FROM command_trust WHERE project_id = ? AND name = ? AND config_hash = ?")
206
+ .get(projectId, name, hash);
207
+ }
165
208
  function startYmlCommand(project, name, proc, config) {
166
209
  if (isReservedAgentName(name)) {
167
210
  return `skipped: "lead" is reserved for this project's lead session and cannot be used as a process name`;
@@ -318,24 +361,39 @@ function reportMigrationResult(result) {
318
361
  console.error(`! ${warning}`);
319
362
  }
320
363
  async function cmdLead(argv) {
321
- const unknownFlag = argv.find((a) => a.startsWith("--") && a !== "--no-dashboard");
364
+ const unknownFlag = argv.find((a) => a.startsWith("--") && a !== "--no-dashboard" && a !== "--detach");
322
365
  if (unknownFlag !== undefined) {
323
- console.error(`hive lead: unknown flag "${unknownFlag}". The only flag is --no-dashboard.`);
366
+ console.error(`hive lead: unknown flag "${unknownFlag}". Flags are --no-dashboard and --detach.`);
324
367
  process.exit(1);
325
368
  }
326
369
  const noDashboard = argv.includes("--no-dashboard");
370
+ const detach = argv.includes("--detach");
327
371
  const path = argv.find((a) => !a.startsWith("--"));
328
372
  let registrationNotice = null;
329
373
  try {
330
- const project = resolveProject(path, (text) => {
374
+ const project = resolveProjectForWrite("hive lead", path, (text) => {
331
375
  registrationNotice = text;
332
376
  });
377
+ const auditIdentity = captureQueenWriteIdentity(project.id);
378
+ const { config, warnings, sources } = loadProjectYml(project.path);
379
+ if (detach && (!process.stdin.isTTY || !process.stdout.isTTY)) {
380
+ let untrusted;
381
+ if (config?.lead && !isTrusted(project.id, "lead", config.lead, null, {}))
382
+ untrusted = "lead";
383
+ if (!untrusted && config) {
384
+ untrusted = Object.entries(config.processes).find(([name, proc]) => proc.auto_start !== false && !isTrusted(project.id, name, proc.command, proc.dir, proc.env))?.[0];
385
+ }
386
+ if (untrusted) {
387
+ console.error(`hive lead: "${untrusted}" is not trusted; run hive lead <path> interactively once`);
388
+ process.exitCode = 1;
389
+ return;
390
+ }
391
+ }
333
392
  reportMigrationResult(migrateLegacyConfig());
334
- const session = sessionName();
335
- const hooksPath = ensureHooksFile();
336
- const { config, warnings } = loadProjectYml(project.path);
337
393
  for (const w of warnings)
338
394
  console.log(`! ${w}`);
395
+ const session = sessionName();
396
+ const hooksPath = ensureHooksFile();
339
397
  let leadCommand = "claude";
340
398
  if (!config) {
341
399
  console.log("No hive.yml here; starting a plain claude lead. Add a hive.yml to define project commands and a custom lead (run hive --help for the format).");
@@ -349,6 +407,10 @@ async function cmdLead(argv) {
349
407
  }
350
408
  }
351
409
  const leadHarness = harnessFor(leadCommand);
410
+ let firstMessage = resolveFirstMessage(config, sources).message;
411
+ if (firstMessage !== "" && !(await (await import("./kickoff.js")).kickoffGate(project.path)).ok)
412
+ firstMessage = "";
413
+ let promptSuffix = "";
352
414
  // Posture is rendered once, then delivered through whichever channel this harness has: a
353
415
  // claude lead gets it as --append-system-prompt-file below; a codex lead gets the identical
354
416
  // text as config.toml's developer_instructions, built after ensureLeadRow (it needs leadActorId).
@@ -386,6 +448,9 @@ async function cmdLead(argv) {
386
448
  leadCommand += ` ${leadHarness.briefDelivery.systemPromptArgs(posturePath).map(shellQuote).join(" ")}`;
387
449
  console.log(`- profile: ${profile} (${postureSource} posture; see it with: hive posture)`);
388
450
  }
451
+ if (leadHarness.initialPromptArgs && firstMessage !== "") {
452
+ promptSuffix = ` ${leadHarness.initialPromptArgs(firstMessage).map(shellQuote).join(" ")}`;
453
+ }
389
454
  }
390
455
  const windowName = project.name;
391
456
  const { agentId: leadAgentId, actorId: leadActorId, previousTarget, previousSocket, previousPanePid, previousCommand, previousCodexHome, casExpected, } = ensureLeadRow(project, leadCommand);
@@ -430,8 +495,8 @@ async function cmdLead(argv) {
430
495
  throw e;
431
496
  }
432
497
  leadCommand += ` ${homeArgs.map(shellQuote).join(" ")}`;
433
- if (leadHarness.initialPromptArgs) {
434
- leadCommand += ` ${leadHarness.initialPromptArgs(TRIAGE_MESSAGE).map(shellQuote).join(" ")}`;
498
+ if (leadHarness.initialPromptArgs && firstMessage !== "") {
499
+ promptSuffix = ` ${leadHarness.initialPromptArgs(firstMessage).map(shellQuote).join(" ")}`;
435
500
  }
436
501
  if (renderedPosture !== null) {
437
502
  console.log(`- profile: ${profile} (${postureSource} posture; see it with: hive posture)`);
@@ -442,13 +507,15 @@ async function cmdLead(argv) {
442
507
  }
443
508
  const envFlags = buildEnvFlags({
444
509
  ...leadEnv,
510
+ [FIRST_MESSAGE_SHA_ENV]: promptSuffix !== "" ? firstMessageDigest(firstMessage) : "",
445
511
  ...(newCodexHomeKey ? { CODEX_HOME: codexHomeDir(newCodexHomeKey) } : {}),
446
512
  });
513
+ const launchCommand = leadCommand + promptSuffix;
447
514
  const { leadPane, leadWindow, createdPane } = withWindowClaim(() => {
448
515
  let leadPane;
449
516
  let leadWindow;
450
517
  let createdPane;
451
- const started = ensureSession(session, project.path, { envFlags, command: leadCommand });
518
+ const started = ensureSession(session, project.path, { envFlags, command: launchCommand });
452
519
  if (started.created) {
453
520
  const claimed = claimInitialWindow(started, windowName, project.id);
454
521
  leadPane = claimed.pane;
@@ -467,13 +534,13 @@ async function cmdLead(argv) {
467
534
  createdPane = false;
468
535
  }
469
536
  else if (!foundWindow) {
470
- const fresh = createWindow(session, windowName, project.path, envFlags, leadCommand, project.id);
537
+ const fresh = createWindow(session, windowName, project.path, envFlags, launchCommand, project.id, detach);
471
538
  leadPane = fresh.pane;
472
539
  leadWindow = fresh.window;
473
540
  createdPane = true;
474
541
  }
475
542
  else {
476
- leadPane = tmux("split-window", "-P", "-F", "#{pane_id}", "-t", foundWindow, "-c", project.path, ...envFlags, leadCommand);
543
+ leadPane = tmux("split-window", ...(detach ? ["-d"] : []), "-P", "-F", "#{pane_id}", "-t", foundWindow, "-c", project.path, ...envFlags, launchCommand);
477
544
  leadWindow = foundWindow;
478
545
  createdPane = true;
479
546
  }
@@ -495,18 +562,7 @@ async function cmdLead(argv) {
495
562
  // also reaps a stale codex home left over from a lead config that has since switched away from
496
563
  // codex, since createdPane's branch does not require leadHarness.needsHome to fire.
497
564
  const recordedCodexHome = createdPane ? (newCodexHomeKey ?? "") : previousCodexHome;
498
- const wonRace = db.transaction(() => {
499
- const updated = db
500
- .prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ?, command = ?, codex_home = ? WHERE id = ? AND tmux_target = ? AND status = 'running'")
501
- .run(leadPane, tmuxSocketPath(process.env.TMUX, process.env.TMUX_TMPDIR), panePid(leadPane), recordedCommand, recordedCodexHome, leadAgentId, casExpected).changes;
502
- if (updated === 0)
503
- return false;
504
- db.prepare(`UPDATE wakes SET deliver_pane = ?, held_at = NULL, held_reason = NULL
505
- WHERE ${ACTIVE_TIMER_WHERE} AND deliver_actor = ?
506
- AND (? = 1 OR held_reason IS NULL OR held_reason NOT LIKE ?)`).run(leadPane, leadActorId, createdPane ? 1 : 0, `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}%`);
507
- return true;
508
- })();
509
- if (!wonRace) {
565
+ const abandonClaim = () => {
510
566
  if (createdPane) {
511
567
  try {
512
568
  waitForPaneEstablished(leadPane);
@@ -517,6 +573,31 @@ async function cmdLead(argv) {
517
573
  }
518
574
  if (newCodexHomeKey)
519
575
  reapCodexHome(newCodexHomeKey);
576
+ };
577
+ let wonRace;
578
+ try {
579
+ wonRace = db.transaction(() => {
580
+ const updated = db
581
+ .prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ?, command = ?, codex_home = ? WHERE id = ? AND tmux_target = ? AND status = 'running'")
582
+ .run(leadPane, tmuxSocketPath(process.env.TMUX, process.env.TMUX_TMPDIR), panePid(leadPane), recordedCommand, recordedCodexHome, leadAgentId, casExpected).changes;
583
+ if (updated === 0)
584
+ return false;
585
+ db.prepare(`UPDATE wakes SET deliver_pane = ?, held_at = NULL, held_reason = NULL
586
+ WHERE ${ACTIVE_TIMER_WHERE} AND deliver_actor = ?
587
+ AND (? = 1 OR held_reason IS NULL OR held_reason NOT LIKE ?)`).run(leadPane, leadActorId, createdPane ? 1 : 0, `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}%`);
588
+ confirmQueenWrite("hive lead", project.id, { detach }, {
589
+ agent_id: leadAgentId,
590
+ disposition: createdPane ? "started" : "adopted",
591
+ }, auditIdentity);
592
+ return true;
593
+ })();
594
+ }
595
+ catch (e) {
596
+ abandonClaim();
597
+ throw e;
598
+ }
599
+ if (!wonRace) {
600
+ abandonClaim();
520
601
  throw new Error("Another `hive lead` won the race to record a live pane for this project's lead session (both saw the " +
521
602
  "same dead pane and both tried to replace it, or this row was closed by another process mid-restart). " +
522
603
  "Re-run `hive lead`; it will attach to the pane that invocation recorded.");
@@ -550,6 +631,11 @@ async function cmdLead(argv) {
550
631
  console.error(registrationNotice);
551
632
  registrationNotice = null;
552
633
  }
634
+ if (detach) {
635
+ console.log(`LEAD_PANE=${leadPane}`);
636
+ console.log(`ATTACH_COMMAND=hive attach ${shellQuote(project.path)}`);
637
+ return;
638
+ }
553
639
  if (!noDashboard)
554
640
  maybeOpenDashboard(project, !!config?.dashboard);
555
641
  attach(session, project, leadWindow);
@@ -560,6 +646,60 @@ async function cmdLead(argv) {
560
646
  throw e;
561
647
  }
562
648
  }
649
+ function publishQueenYml(home) {
650
+ const yml = join(home, "hive.yml");
651
+ const staged = join(home, `.hive.yml.${process.pid}.${randomUUID()}`);
652
+ writeFileSync(staged, `profile: ${QUEEN_PROFILE}\n`, { flag: "wx" });
653
+ try {
654
+ linkSync(staged, yml);
655
+ }
656
+ catch (e) {
657
+ if (e.code !== "EEXIST")
658
+ throw e;
659
+ }
660
+ finally {
661
+ unlinkSync(staged);
662
+ }
663
+ if (lstatSync(yml).isSymbolicLink()) {
664
+ throw new Error(`hive queen: ${yml} is a symlink. The queen's config must be a real file; remove the link and run hive queen again.`);
665
+ }
666
+ const { config } = loadProjectYml(home);
667
+ if (activeProfile(config) !== QUEEN_PROFILE) {
668
+ throw new Error(`hive queen: ${yml} already exists and does not select "profile: ${QUEEN_PROFILE}". hive will not ` +
669
+ "overwrite it. Add that line yourself, or move the file away and run hive queen again.");
670
+ }
671
+ }
672
+ async function cmdQueen(argv) {
673
+ const parsed = parseArgs(argv, { flags: ["--no-dashboard"] });
674
+ rejectUnknownFlags("queen", parsed, "--no-dashboard");
675
+ if (parsed.positional.length > 0) {
676
+ console.error(`hive queen: takes no path. The queen lives in the data dir (${queenHomeDir()}).`);
677
+ process.exit(1);
678
+ }
679
+ if (process.env.HIVE_PROJECT_LOCK === "1" || agentProjectPin() != null) {
680
+ throw new Error("hive queen: this session is locked to its own project (HIVE_PROJECT_LOCK=1), so it cannot create or " +
681
+ "start the queen. Run hive queen from your own terminal.");
682
+ }
683
+ const home = queenHomeDir();
684
+ let existing = null;
685
+ try {
686
+ existing = lstatSync(home);
687
+ }
688
+ catch {
689
+ existing = null;
690
+ }
691
+ if (existing?.isSymbolicLink() || (existing && !existing.isDirectory())) {
692
+ throw new Error(`hive queen: ${home} exists and is not a plain directory. The queen's home must be a real directory ` +
693
+ "inside the data dir; move it away and run hive queen again.");
694
+ }
695
+ mkdirSync(home, { recursive: true });
696
+ publishQueenYml(home);
697
+ const queen = addProject(home, "queen");
698
+ generateQueenDashboardNow();
699
+ if (!parsed.flags.has("--no-dashboard"))
700
+ maybeOpenQueenDashboard(queen);
701
+ await cmdLead([home, ...parsed.flags]);
702
+ }
563
703
  const HIVE_YML_TEMPLATE = `# hive project config. Read by \`hive lead\` from the project root.
564
704
  # Commands defined here run only after a one-time interactive approval,
565
705
  # and re-require it whenever they change.
@@ -577,6 +717,7 @@ const HIVE_YML_TEMPLATE = `# hive project config. Read by \`hive lead\` from the
577
717
  # agents: [claude, codex] # crew harness pool; the first entry is the default
578
718
 
579
719
  # lead_branches: [main, master] # branches where a session gets hive's kickoff
720
+ # first_message: "..." # a lead's first turn; absent or "" sends none
580
721
 
581
722
  # context_checkpoint_percent: null # unset means off; integer 1-100 to enable
582
723
 
@@ -778,6 +919,7 @@ async function cmdInit(argv) {
778
919
  process.exit(1);
779
920
  }
780
921
  const existing = getProjectByPath(registration);
922
+ assertQueenCrossProjectWrite("hive init", existing?.id ?? null, {});
781
923
  project = existing ?? addProject(registration);
782
924
  newlyRegistered = existing == null;
783
925
  }
@@ -894,7 +1036,7 @@ itself to this project before any real work runs.`);
894
1036
  }
895
1037
  const NO_RUNBOOK_PAD_MESSAGE = `is on "profile: none" but has no runbook pad. Create one with: hive init`;
896
1038
  function cmdRunbook(path) {
897
- const project = resolveProject(path);
1039
+ const project = resolveProjectForRead("runbook", path);
898
1040
  const { config, warnings } = loadProjectYml(project.path);
899
1041
  for (const w of warnings)
900
1042
  console.log(`! ${w}`);
@@ -919,7 +1061,7 @@ function cmdRunbook(path) {
919
1061
  process.stdout.write(withTrailingNewline(rendered));
920
1062
  }
921
1063
  function cmdPosture(path) {
922
- const project = resolveProject(path);
1064
+ const project = resolveProjectForRead("posture", path);
923
1065
  const { config, warnings } = loadProjectYml(project.path);
924
1066
  for (const w of warnings)
925
1067
  console.log(`! ${w}`);
@@ -1060,7 +1202,7 @@ function cmdProfile(argv) {
1060
1202
  vars = mergedProjectVars(here ? loadProjectYml(here.path).config : null);
1061
1203
  }
1062
1204
  else {
1063
- const project = resolveProject();
1205
+ const project = resolveProjectForRead("profile read");
1064
1206
  const { config, warnings } = loadProjectYml(project.path);
1065
1207
  for (const w of warnings)
1066
1208
  console.log(`! ${w}`);
@@ -1107,6 +1249,22 @@ function maybeOpenDashboard(project, dashboardEnabled) {
1107
1249
  return;
1108
1250
  if (!dashboardFileContained(dashboardFile, project.path))
1109
1251
  return;
1252
+ openDashboardOnce(project, dashboardFile);
1253
+ }
1254
+ function maybeOpenQueenDashboard(project) {
1255
+ if (process.platform !== "darwin")
1256
+ return;
1257
+ const dashboardFile = join(project.path, "dashboard.html");
1258
+ try {
1259
+ if (!existsSync(dashboardFile) || !dashboardFileContained(dashboardFile, project.path))
1260
+ return;
1261
+ }
1262
+ catch {
1263
+ return;
1264
+ }
1265
+ openDashboardOnce(project, dashboardFile);
1266
+ }
1267
+ function openDashboardOnce(project, dashboardFile) {
1110
1268
  const actor = currentActor();
1111
1269
  const claim = db
1112
1270
  .prepare(`INSERT INTO kv (project_id, key, value, updated_by, expires_at)
@@ -1132,7 +1290,7 @@ function cmdAttach(argv) {
1132
1290
  process.exit(1);
1133
1291
  }
1134
1292
  const path = argv.find((a) => !a.startsWith("--"));
1135
- const project = resolveProject(path);
1293
+ const project = resolveProjectForWrite("hive attach", path);
1136
1294
  maybeOpenDashboard(project, !!loadProjectYml(project.path).config?.dashboard);
1137
1295
  const session = sessionName();
1138
1296
  const window = withWindowClaim(() => {
@@ -1160,7 +1318,7 @@ async function cmdStart(argv) {
1160
1318
  console.log("Usage: hive start <process> [path]");
1161
1319
  process.exit(1);
1162
1320
  }
1163
- const project = resolveProject(path);
1321
+ const project = resolveProjectForWrite("hive start", path);
1164
1322
  const { config, warnings } = loadProjectYml(project.path);
1165
1323
  for (const w of warnings)
1166
1324
  console.log(`! ${w}`);
@@ -1188,7 +1346,7 @@ function resolveNamedProcess(argv, verb) {
1188
1346
  console.log(`Usage: hive ${verb} <process> [path]`);
1189
1347
  process.exit(1);
1190
1348
  }
1191
- const project = resolveProject(parsed.positional[1]);
1349
+ const project = resolveProjectForWrite(`hive ${verb}`, parsed.positional[1]);
1192
1350
  const { config, warnings } = loadProjectYml(project.path);
1193
1351
  for (const w of warnings)
1194
1352
  console.log(`! ${w}`);
@@ -1228,7 +1386,7 @@ function leadPaneExitedHookCommand() {
1228
1386
  // started from inside a worker's pane hands them to every run-shell it ever runs, where a stale
1229
1387
  // HIVE_AGENT_ID is a hard refusal before the verb is reached at all. The same clearing
1230
1388
  // scripts/restart-lead.sh does around its own `hive lead`, plus the two project vars.
1231
- const clear = ["HIVE_AGENT_ID", "HIVE_AGENT_NAME", "HIVE_LEAD", "HIVE_PROJECT_LOCK", "HIVE_PROJECT_PATH"]
1389
+ const clear = ["HIVE_AGENT_ID", "HIVE_AGENT_NAME", "HIVE_LEAD", FIRST_MESSAGE_SHA_ENV, "HIVE_PROJECT_LOCK", "HIVE_PROJECT_PATH"]
1232
1390
  .flatMap((name) => ["-u", name]);
1233
1391
  // Redirected because tmux prints a run-shell's output into the pane it fires from, which is a
1234
1392
  // human's own window: a backstop that failed to start must not type into it.
@@ -1310,7 +1468,7 @@ function cmdStop(argv) {
1310
1468
  const parsed = parseArgs(argv, { flags: ["--all"] });
1311
1469
  rejectUnknownFlags("stop", parsed, "--all");
1312
1470
  if (parsed.flags.has("--all")) {
1313
- const project = resolveProject(parsed.positional[0]);
1471
+ const project = resolveProjectForWrite("hive stop", parsed.positional[0]);
1314
1472
  const stopped = stopAllProcesses(project.id, STOP_REASONS.byHand);
1315
1473
  console.log(stopped.length === 0 ? "No processes are running." : stopped.map(stopLine).join("\n"));
1316
1474
  return;
@@ -1320,7 +1478,7 @@ function cmdStop(argv) {
1320
1478
  console.log("Usage: hive stop <process> [path] | hive stop --all [path]");
1321
1479
  process.exit(1);
1322
1480
  }
1323
- const project = resolveProject(parsed.positional[1]);
1481
+ const project = resolveProjectForWrite("hive stop", parsed.positional[1]);
1324
1482
  const row = runningCommandRow(project.id, name);
1325
1483
  if (!row) {
1326
1484
  const { config, warnings } = loadProjectYml(project.path);
@@ -1723,7 +1881,7 @@ function reportProjectScope(here) {
1723
1881
  if (locked && project.id !== here?.id)
1724
1882
  continue;
1725
1883
  if (project.path === homePath || project.path === "/") {
1726
- warn("project scope", `"${project.name}" (${project.path}) is a home directory; every unregistered directory under it resolves to this project. hive project prune removes it once it holds nothing.`);
1884
+ warn("project scope", `"${project.name}" (${project.path}) is a home directory; every unregistered directory under it resolves to this project. The project_prune tool with this project's id (from an unlocked session in another project) removes it once it holds nothing.`);
1727
1885
  }
1728
1886
  }
1729
1887
  for (const parent of projects) {
@@ -2595,6 +2753,195 @@ function findPadExports(projectId, name) {
2595
2753
  .filter((f) => f.startsWith(prefix) && f.endsWith(".md"))
2596
2754
  .map((f) => join(tmpdir(), f));
2597
2755
  }
2756
+ function canonicalPath(path) {
2757
+ try {
2758
+ return realpathSync(path);
2759
+ }
2760
+ catch {
2761
+ return resolve(path);
2762
+ }
2763
+ }
2764
+ function nextCandidates() {
2765
+ const queenRoot = canonicalPath(join(storeDir(), "queen"));
2766
+ const byCount = (a, b) => b - a;
2767
+ return collectPortfolio()
2768
+ .projects.filter((p) => (p.lane === "waiting_on_you" || p.lane === "stuck" || (p.lane === "moving" && p.needs_human > 0)) &&
2769
+ canonicalPath(p.root) !== queenRoot)
2770
+ .sort((a, b) => {
2771
+ const tier = (p) => (p.lane === "waiting_on_you" ? 0 : p.needs_human > 0 ? 1 : 2);
2772
+ if (tier(a) !== tier(b))
2773
+ return tier(a) - tier(b);
2774
+ if (a.lane === "waiting_on_you") {
2775
+ return (byCount(a.needs_human, b.needs_human) ||
2776
+ a.last_activity_at.localeCompare(b.last_activity_at) ||
2777
+ a.id - b.id);
2778
+ }
2779
+ return (byCount(a.needs_human, b.needs_human) ||
2780
+ byCount(a.todos.blocked_in_progress, b.todos.blocked_in_progress) ||
2781
+ byCount(a.wakes.overdue, b.wakes.overdue) ||
2782
+ byCount(a.workers.needs_input, b.workers.needs_input) ||
2783
+ a.last_activity_at.localeCompare(b.last_activity_at) ||
2784
+ a.id - b.id);
2785
+ });
2786
+ }
2787
+ function collectProjectById(id) {
2788
+ return collectPortfolio().projects.find((p) => p.id === id);
2789
+ }
2790
+ async function cmdNext(argv) {
2791
+ const parsed = parseArgs(argv, { flags: ["--print"] });
2792
+ if (parsed.positional.length > 0)
2793
+ parsed.unknown.push(parsed.positional[0]);
2794
+ rejectUnknownFlags("next", parsed, "--print");
2795
+ if (process.env.HIVE_PROJECT_LOCK === "1") {
2796
+ console.error("hive next: this session is locked to one project (HIVE_PROJECT_LOCK=1) and cannot jump to another. Run it from a pane outside a worker session.");
2797
+ process.exit(1);
2798
+ }
2799
+ const chosen = nextCandidates()[0];
2800
+ if (!chosen) {
2801
+ console.log("No project needs you: none is waiting, stuck, or holding a needs-human item.");
2802
+ return;
2803
+ }
2804
+ console.log(JSON.stringify({
2805
+ project_id: chosen.id,
2806
+ name: chosen.name,
2807
+ root: chosen.root,
2808
+ lane: chosen.lane,
2809
+ reasons: chosen.reasons,
2810
+ lead_state: chosen.lead.state,
2811
+ }));
2812
+ if (parsed.flags.has("--print"))
2813
+ return;
2814
+ assertQueenCrossProjectWrite("hive next", chosen.id, {});
2815
+ if (!chosen.root_exists) {
2816
+ console.error(`hive next: project ${chosen.id} ("${chosen.name}") root is missing: ${chosen.root}. Remove the stale project with the project_prune tool, or restore the directory.`);
2817
+ process.exit(1);
2818
+ }
2819
+ let state = collectProjectById(chosen.id)?.lead.state ?? "none";
2820
+ if (state === "unknown") {
2821
+ console.error(`hive next: cannot tell whether the lead for project ${chosen.id} ("${chosen.name}") is alive (tmux did not answer for its socket). Starting nothing.`);
2822
+ process.exit(1);
2823
+ }
2824
+ await cmdLead([chosen.root, "--detach"]);
2825
+ state = collectProjectById(chosen.id)?.lead.state ?? "none";
2826
+ if (state !== "alive" || (process.exitCode !== undefined && process.exitCode !== 0)) {
2827
+ console.error(`hive next: the lead for project ${chosen.id} ("${chosen.name}") did not start cleanly (${state}). Not attaching.`);
2828
+ process.exit(1);
2829
+ }
2830
+ cmdAttach([chosen.root]);
2831
+ }
2832
+ function cmdQueenAudit(argv) {
2833
+ const parsed = parseArgs(argv, { flags: ["--json"], valued: ["--project-id", "--limit"] });
2834
+ if (parsed.positional.length > 0)
2835
+ parsed.unknown.push(parsed.positional[0]);
2836
+ rejectUnknownFlags("queen-audit", parsed, "--project-id <id>, --limit <n>, --json");
2837
+ requireFlagValues("queen-audit", parsed);
2838
+ let report;
2839
+ try {
2840
+ const project = pinnedOrCwdProject();
2841
+ if (!project)
2842
+ throw new Error("hive queen-audit: the cwd is not a registered project. Run this command from a registered project.");
2843
+ report = listQueenAudit({
2844
+ project_id: parsed.values.has("--project-id") ? Number(parsed.values.get("--project-id")) : undefined,
2845
+ limit: parsed.values.has("--limit") ? Number(parsed.values.get("--limit")) : undefined,
2846
+ }, project.id);
2847
+ }
2848
+ catch (e) {
2849
+ console.error(errorMessage(e));
2850
+ process.exitCode = 1;
2851
+ return;
2852
+ }
2853
+ if (parsed.flags.has("--json")) {
2854
+ console.log(JSON.stringify(report));
2855
+ return;
2856
+ }
2857
+ if (report.entries.length === 0) {
2858
+ console.log("No queen actions recorded.");
2859
+ return;
2860
+ }
2861
+ for (const entry of report.entries) {
2862
+ console.log(`${entry.created_at} UTC ${entry.actor_id} -> target #${entry.target_project_id}\n` +
2863
+ ` ${entry.operation} ${entry.resource_type} #${entry.resource_id}\n ${entry.summary}`);
2864
+ }
2865
+ }
2866
+ function cmdPortfolio(argv) {
2867
+ const parsed = parseArgs(argv, { flags: ["--json"] });
2868
+ if (parsed.positional.length > 0)
2869
+ parsed.unknown.push(parsed.positional[0]);
2870
+ rejectUnknownFlags("portfolio", parsed, "--json");
2871
+ const report = collectPortfolio();
2872
+ if (parsed.flags.has("--json")) {
2873
+ console.log(JSON.stringify(report));
2874
+ return;
2875
+ }
2876
+ if (report.projects.length === 0) {
2877
+ console.log("No registered projects.");
2878
+ return;
2879
+ }
2880
+ for (const p of report.projects) {
2881
+ const w = p.workers;
2882
+ const t = p.todos;
2883
+ console.log(`${p.lane.padEnd(14)} ${p.name} (#${p.id})${p.root_exists ? "" : " [root missing]"}\n` +
2884
+ ` why: ${p.reasons.join(", ")}\n` +
2885
+ ` lead ${leadText(p)}; workers ${w.working} working, ${w.idle} idle, ${w.needs_input} needs input, ` +
2886
+ `${w.other} other, ${w.unreachable} unreachable, ${w.unconfirmed} unconfirmed\n` +
2887
+ ` todos ${t.open} open, ${t.in_progress} in progress, ${t.blocked} blocked, ${t.high} high; ` +
2888
+ `needs human ${p.needs_human}; wakes ${p.wakes.pending} pending, ${p.wakes.overdue} overdue\n` +
2889
+ ` last activity ${p.last_activity_at} UTC`);
2890
+ }
2891
+ const l = report.totals.lanes;
2892
+ console.log(`\n${report.totals.projects} project(s): ${l.waiting_on_you} waiting on you, ${l.stuck} stuck, ${l.moving} moving, ${l.quiet} quiet`);
2893
+ }
2894
+ async function cmdProject(argv) {
2895
+ const parsed = parseArgs(argv, { flags: ["--yes", "-y"] });
2896
+ rejectUnknownFlags("project", parsed, "--yes/-y");
2897
+ const [verb, ref] = parsed.positional;
2898
+ if (verb !== "rm" || !ref || parsed.positional.length > 2) {
2899
+ console.log("Usage: hive project rm <id|path> [--yes]");
2900
+ process.exit(1);
2901
+ }
2902
+ if (process.env.HIVE_PROJECT_LOCK === "1" || agentProjectPin() != null) {
2903
+ console.log("hive project rm: this session is locked to one project (HIVE_PROJECT_LOCK=1) and cannot remove projects.");
2904
+ process.exit(1);
2905
+ }
2906
+ let target;
2907
+ if (/^\d+$/.test(ref))
2908
+ target = getProject(Number(ref));
2909
+ else
2910
+ target = getProjectByPath(ref) ?? getProjectByPath(canonicalPath(ref));
2911
+ if (!target) {
2912
+ console.log(`hive project rm: "${ref}" is not a registered project id or exact registered path. List them with the project_list tool.`);
2913
+ process.exit(1);
2914
+ }
2915
+ if (findProjectForCwd()?.id === target.id) {
2916
+ console.log(`hive project rm: project ${target.id} ("${target.name}") is the one this directory belongs to and is never removed from itself. Run it from another project.`);
2917
+ process.exit(1);
2918
+ }
2919
+ assertQueenCrossProjectWrite("hive project rm", target.id, {});
2920
+ const counts = projectRowCounts(target.id);
2921
+ console.log(`Project ${target.id}: "${target.name}" (${target.path})`);
2922
+ console.log(Object.keys(counts).length > 0 ? `It owns ${formatRowCounts(counts)}.` : "It owns no rows.");
2923
+ console.log("A snapshot of the store is taken first. Restore it with: hive restore <name>");
2924
+ if (!parsed.flags.has("--yes") && !parsed.flags.has("-y")) {
2925
+ const confirmed = await confirmYesNo("Remove this project and everything it owns? [y/N] ");
2926
+ if (confirmed === null) {
2927
+ console.log("Not removed: run hive interactively to confirm, or pass --yes.");
2928
+ process.exit(1);
2929
+ }
2930
+ if (!confirmed) {
2931
+ console.log("Not removed.");
2932
+ process.exit(1);
2933
+ }
2934
+ }
2935
+ const removed = removeProject(target.id, {
2936
+ snapshot: true,
2937
+ onRemoved: (r) => confirmQueenWrite("hive project rm", r.deleted.id, {}, { project_id: r.deleted.id, name: r.deleted.name, counts: r.counts }),
2938
+ });
2939
+ console.log(`Removed project ${removed.deleted.id} ("${removed.deleted.name}").`);
2940
+ if (Object.keys(removed.counts).length > 0)
2941
+ console.log(`Deleted ${formatRowCounts(removed.counts)}.`);
2942
+ console.log(`Snapshot: ${removed.snapshot}`);
2943
+ console.log("Its actors and any files it left on disk are untouched; actor_prune removes inert actors.");
2944
+ }
2598
2945
  function cmdBackups() {
2599
2946
  const snapshots = listSnapshots(dataDir);
2600
2947
  if (snapshots.length === 0) {
@@ -2762,9 +3109,7 @@ function requireFlagValues(command, parsed) {
2762
3109
  process.exit(1);
2763
3110
  }
2764
3111
  function cmdTodos(argv) {
2765
- const project = pinnedOrCwdProject();
2766
- if (!project)
2767
- return;
3112
+ const project = resolveProjectForRead("todos");
2768
3113
  const all = argv.includes("--all");
2769
3114
  const statusIdx = argv.indexOf("--status");
2770
3115
  const status = flagValue(argv, "--status");
@@ -2808,9 +3153,7 @@ function cmdTodos(argv) {
2808
3153
  }
2809
3154
  }
2810
3155
  function cmdTodo(argv) {
2811
- const project = pinnedOrCwdProject();
2812
- if (!project)
2813
- return;
3156
+ const project = resolveProjectForRead("todo");
2814
3157
  const id = Number(argv.find((a) => !a.startsWith("--")));
2815
3158
  if (!Number.isInteger(id)) {
2816
3159
  console.log("Usage: hive todo <id> (run inside the project)");
@@ -2851,7 +3194,7 @@ function cmdTodo(argv) {
2851
3194
  }
2852
3195
  }
2853
3196
  function cmdPads() {
2854
- const project = resolveProject();
3197
+ const project = resolveProjectForRead("pads");
2855
3198
  const pads = listActivePads(project.id);
2856
3199
  if (pads.length === 0) {
2857
3200
  console.log(`No pads in project "${project.name}".`);
@@ -2870,7 +3213,7 @@ function cmdPad(argv) {
2870
3213
  console.log("Usage: hive pad <name> [--edit | --save [file]] (run inside the project)");
2871
3214
  process.exit(1);
2872
3215
  }
2873
- const project = resolveProject();
3216
+ const project = parsed.flags.has("--save") ? resolveProjectForWrite("hive pad --save") : resolveProjectForRead("pad");
2874
3217
  const pad = getActivePadByName(project.id, name);
2875
3218
  if (!pad) {
2876
3219
  console.log(`No pad named "${name}" in project "${project.name}". List them with: hive pads`);
@@ -2940,6 +3283,11 @@ function cmdPad(argv) {
2940
3283
  }
2941
3284
  process.stdout.write(withTrailingNewline(pad.content));
2942
3285
  }
3286
+ process.stdout.on("error", (error) => {
3287
+ if (error.code === "EPIPE")
3288
+ process.exit(0);
3289
+ throw error;
3290
+ });
2943
3291
  const args = process.argv.slice(2);
2944
3292
  let command = args[0] ?? "lead";
2945
3293
  let rest = args.slice(1);
@@ -2964,9 +3312,9 @@ if (command === "--version" || command === "-v") {
2964
3312
  process.exit(0);
2965
3313
  }
2966
3314
  const COMMANDS = [
2967
- "lead", "init", "attach", "start", "stop", "show", "hide", "status", "setup", "upgrade", "doctor",
3315
+ "lead", "queen", "queen-audit", "init", "attach", "start", "stop", "show", "hide", "status", "portfolio", "next", "setup", "upgrade", "doctor",
2968
3316
  LEAD_PANE_EXITED_VERB,
2969
- "pads", "pad", "todos", "todo", "backups", "restore", "runbook", "posture", "profile", "kickoff", "statusline",
3317
+ "pads", "pad", "todos", "todo", "backups", "restore", "project", "runbook", "posture", "profile", "kickoff", "statusline",
2970
3318
  ];
2971
3319
  if (!COMMANDS.includes(command)) {
2972
3320
  if (command.startsWith("--")) {
@@ -2988,6 +3336,9 @@ try {
2988
3336
  case "lead":
2989
3337
  await cmdLead(rest);
2990
3338
  break;
3339
+ case "queen":
3340
+ await cmdQueen(rest);
3341
+ break;
2991
3342
  case "init":
2992
3343
  await cmdInit(rest);
2993
3344
  break;
@@ -3012,6 +3363,15 @@ try {
3012
3363
  case "status":
3013
3364
  cmdStatus();
3014
3365
  break;
3366
+ case "portfolio":
3367
+ cmdPortfolio(rest);
3368
+ break;
3369
+ case "queen-audit":
3370
+ cmdQueenAudit(rest);
3371
+ break;
3372
+ case "next":
3373
+ await cmdNext(rest);
3374
+ break;
3015
3375
  case "setup":
3016
3376
  cmdSetup(rest);
3017
3377
  break;
@@ -3039,6 +3399,9 @@ try {
3039
3399
  case "restore":
3040
3400
  await cmdRestore(rest);
3041
3401
  break;
3402
+ case "project":
3403
+ await cmdProject(rest);
3404
+ break;
3042
3405
  case "runbook":
3043
3406
  cmdRunbook(rest[0]);
3044
3407
  break;