@cmgmyr/hive 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/claude-plugin/README.md +1 -1
- package/dist/brief.js +1 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.js +360 -55
- package/dist/context.js +191 -2
- package/dist/db.js +61 -18
- package/dist/firstMessage.js +5 -0
- package/dist/harnesses.js +6 -4
- package/dist/help.js +28 -10
- package/dist/hook.js +9 -0
- package/dist/index.js +11 -5
- package/dist/kickoff.js +41 -27
- package/dist/leadState.js +63 -0
- package/dist/leadWatch.js +73 -0
- package/dist/portfolio.js +210 -0
- package/dist/projectYml.js +19 -0
- package/dist/queenAudit.js +118 -0
- package/dist/queenDashboard.js +573 -0
- package/dist/scheduler.js +179 -15
- package/dist/strictInput.js +24 -7
- package/dist/tmux.js +65 -0
- package/dist/tools/agents.js +4 -0
- package/dist/tools/leases.js +2 -2
- package/dist/tools/meta.js +23 -3
- package/dist/tools/queenAudit.js +13 -0
- package/dist/tools/todos.js +18 -13
- package/dist/tools/wakes.js +197 -65
- package/package.json +4 -4
- package/profiles/orchestration/worker.md +1 -1
- package/profiles/queen/posture.md +31 -0
- package/profiles/queen/runbook.md +26 -0
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,18 @@ 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";
|
|
21
22
|
import { ensureHooksFile } from "./hooks.js";
|
|
22
23
|
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";
|
|
24
|
+
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
25
|
import { readTurnCount } from "./turnCount.js";
|
|
26
|
+
import { collectPortfolio, leadText } from "./portfolio.js";
|
|
25
27
|
import { STALL_BOUND_SECONDS } from "./backgroundTasks.js";
|
|
26
28
|
import { probeSessionInterpreter, reexecTarget, sessionStartVerdict, } from "./sessionProbe.js";
|
|
27
29
|
import { backupHealth, backupNow, backupsDir, formatBytes, listSnapshots, previewRestore, restoreSnapshot, totalSizeBytes, } from "./backup.js";
|
|
@@ -30,11 +32,11 @@ import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchA
|
|
|
30
32
|
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
33
|
import { describeVisibility, processCounts } from "./dashboard.js";
|
|
32
34
|
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";
|
|
35
|
+
import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
|
|
34
36
|
import { writeProjectPosture } from "./brief.js";
|
|
35
37
|
import { carriesNameFlag, harnessFor, hasTranscriptSignal, paneClassifierFor, transcriptDirFor } from "./harnesses.js";
|
|
36
38
|
import { codexHomeDir, codexInstructionsPhrase, ensureCodexHome, reapCodexHome } from "./codexHome.js";
|
|
37
|
-
import {
|
|
39
|
+
import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
|
|
38
40
|
import { ageSecondsSince, deriveProvenance, describeForHuman, describeLastLogEvent, humanizeAge, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "./stateProvenance.js";
|
|
39
41
|
import { awaitingFirstPromptSql } from "./firstPrompt.js";
|
|
40
42
|
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 +48,16 @@ function usage() {
|
|
|
46
48
|
Usage:
|
|
47
49
|
hive --version version, short sha, and dirty marker for this build
|
|
48
50
|
hive [path] open the project's session with a lead window
|
|
49
|
-
hive lead [path] [--no-dashboard]
|
|
51
|
+
hive lead [path] [--no-dashboard] [--detach]
|
|
50
52
|
same; lead is the default command. --no-dashboard
|
|
51
53
|
skips this run's dashboard auto-open -
|
|
52
54
|
scripts/restart-lead.sh passes it; a human rarely
|
|
53
|
-
needs
|
|
55
|
+
needs it. --detach starts or adopts the lead and
|
|
56
|
+
prints its pane and attach command without attaching
|
|
57
|
+
hive queen [--no-dashboard]
|
|
58
|
+
start, or reattach to, the lead that reads across
|
|
59
|
+
every registered project; its home is
|
|
60
|
+
<data dir>/queen, created on first run
|
|
54
61
|
hive init [path] [--profile <name>|--no-profile]
|
|
55
62
|
set the project up: hive.yml, profile, starter pads
|
|
56
63
|
hive attach [path] attach without adding windows
|
|
@@ -59,6 +66,14 @@ Usage:
|
|
|
59
66
|
hive show <process> [path] move a running process's pane beside the lead
|
|
60
67
|
hive hide <process> [path] move it back into the <project>/processes window
|
|
61
68
|
hive status overview of agents, todos, and wake-ups everywhere
|
|
69
|
+
hive portfolio [--json] one row per registered project: lane, lead, workers,
|
|
70
|
+
todos, needs-human count, wakes; read-only
|
|
71
|
+
hive queen-audit confirmed queen writes, newest first; 30-day history
|
|
72
|
+
[--project-id <id>] filter the target project (queen only across projects)
|
|
73
|
+
[--limit <n>] [--json] default 20, maximum 100; JSON prints one object
|
|
74
|
+
hive next [--print] attach to the lead of the project that needs you most
|
|
75
|
+
(waiting on you, then stuck), starting a dead or
|
|
76
|
+
missing lead first; --print only names the choice
|
|
62
77
|
hive upgrade [--check] upgrade a global npm install; preview with --check
|
|
63
78
|
hive upgrade --run run the printed pull/install/build/setup checkout recipe
|
|
64
79
|
hive setup [--dir <dir>] write a \`hive\` that runs the interpreter this build
|
|
@@ -124,6 +139,27 @@ function resolveProject(path, onNotice) {
|
|
|
124
139
|
const pinnedProject = getProject(pinned);
|
|
125
140
|
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
141
|
}
|
|
142
|
+
function resolveProjectForWrite(verb, path, onNotice) {
|
|
143
|
+
assertQueenCrossProjectWrite(verb, findProjectForDir(path ?? process.cwd())?.id ?? null, {});
|
|
144
|
+
return resolveProject(path, onNotice);
|
|
145
|
+
}
|
|
146
|
+
function resolveProjectForRead(verb, path) {
|
|
147
|
+
if (path) {
|
|
148
|
+
process.chdir(path);
|
|
149
|
+
const pinned = agentProjectPin();
|
|
150
|
+
if (pinned != null) {
|
|
151
|
+
const target = findProjectForCwd();
|
|
152
|
+
if (target == null || target.id !== pinned) {
|
|
153
|
+
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.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const project = pinnedOrCwdProject();
|
|
158
|
+
if (project)
|
|
159
|
+
return project;
|
|
160
|
+
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.`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
127
163
|
function pinnedOrCwdProject() {
|
|
128
164
|
const pinned = agentProjectPin();
|
|
129
165
|
if (pinned != null)
|
|
@@ -139,12 +175,9 @@ async function confirmYesNo(question) {
|
|
|
139
175
|
return /^y(es)?$/i.test(answer.trim());
|
|
140
176
|
}
|
|
141
177
|
async function ensureTrusted(projectId, name, command, dir, env) {
|
|
142
|
-
|
|
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)
|
|
178
|
+
if (isTrusted(projectId, name, command, dir, env))
|
|
147
179
|
return true;
|
|
180
|
+
const hash = configHash(name, command, dir, env);
|
|
148
181
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
149
182
|
console.log(`! "${name}" is not trusted yet; run hive interactively to review it.`);
|
|
150
183
|
return false;
|
|
@@ -162,6 +195,12 @@ async function ensureTrusted(projectId, name, command, dir, env) {
|
|
|
162
195
|
db.prepare("INSERT OR IGNORE INTO command_trust (project_id, name, config_hash) VALUES (?, ?, ?)").run(projectId, name, hash);
|
|
163
196
|
return true;
|
|
164
197
|
}
|
|
198
|
+
function isTrusted(projectId, name, command, dir, env) {
|
|
199
|
+
const hash = configHash(name, command, dir, env);
|
|
200
|
+
return !!db
|
|
201
|
+
.prepare("SELECT 1 FROM command_trust WHERE project_id = ? AND name = ? AND config_hash = ?")
|
|
202
|
+
.get(projectId, name, hash);
|
|
203
|
+
}
|
|
165
204
|
function startYmlCommand(project, name, proc, config) {
|
|
166
205
|
if (isReservedAgentName(name)) {
|
|
167
206
|
return `skipped: "lead" is reserved for this project's lead session and cannot be used as a process name`;
|
|
@@ -318,24 +357,39 @@ function reportMigrationResult(result) {
|
|
|
318
357
|
console.error(`! ${warning}`);
|
|
319
358
|
}
|
|
320
359
|
async function cmdLead(argv) {
|
|
321
|
-
const unknownFlag = argv.find((a) => a.startsWith("--") && a !== "--no-dashboard");
|
|
360
|
+
const unknownFlag = argv.find((a) => a.startsWith("--") && a !== "--no-dashboard" && a !== "--detach");
|
|
322
361
|
if (unknownFlag !== undefined) {
|
|
323
|
-
console.error(`hive lead: unknown flag "${unknownFlag}".
|
|
362
|
+
console.error(`hive lead: unknown flag "${unknownFlag}". Flags are --no-dashboard and --detach.`);
|
|
324
363
|
process.exit(1);
|
|
325
364
|
}
|
|
326
365
|
const noDashboard = argv.includes("--no-dashboard");
|
|
366
|
+
const detach = argv.includes("--detach");
|
|
327
367
|
const path = argv.find((a) => !a.startsWith("--"));
|
|
328
368
|
let registrationNotice = null;
|
|
329
369
|
try {
|
|
330
|
-
const project =
|
|
370
|
+
const project = resolveProjectForWrite("hive lead", path, (text) => {
|
|
331
371
|
registrationNotice = text;
|
|
332
372
|
});
|
|
373
|
+
const auditIdentity = captureQueenWriteIdentity(project.id);
|
|
374
|
+
const { config, warnings, sources } = loadProjectYml(project.path);
|
|
375
|
+
if (detach && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
376
|
+
let untrusted;
|
|
377
|
+
if (config?.lead && !isTrusted(project.id, "lead", config.lead, null, {}))
|
|
378
|
+
untrusted = "lead";
|
|
379
|
+
if (!untrusted && config) {
|
|
380
|
+
untrusted = Object.entries(config.processes).find(([name, proc]) => proc.auto_start !== false && !isTrusted(project.id, name, proc.command, proc.dir, proc.env))?.[0];
|
|
381
|
+
}
|
|
382
|
+
if (untrusted) {
|
|
383
|
+
console.error(`hive lead: "${untrusted}" is not trusted; run hive lead <path> interactively once`);
|
|
384
|
+
process.exitCode = 1;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
333
388
|
reportMigrationResult(migrateLegacyConfig());
|
|
334
|
-
const session = sessionName();
|
|
335
|
-
const hooksPath = ensureHooksFile();
|
|
336
|
-
const { config, warnings } = loadProjectYml(project.path);
|
|
337
389
|
for (const w of warnings)
|
|
338
390
|
console.log(`! ${w}`);
|
|
391
|
+
const session = sessionName();
|
|
392
|
+
const hooksPath = ensureHooksFile();
|
|
339
393
|
let leadCommand = "claude";
|
|
340
394
|
if (!config) {
|
|
341
395
|
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 +403,10 @@ async function cmdLead(argv) {
|
|
|
349
403
|
}
|
|
350
404
|
}
|
|
351
405
|
const leadHarness = harnessFor(leadCommand);
|
|
406
|
+
let firstMessage = resolveFirstMessage(config, sources).message;
|
|
407
|
+
if (firstMessage !== "" && !(await (await import("./kickoff.js")).kickoffGate(project.path)).ok)
|
|
408
|
+
firstMessage = "";
|
|
409
|
+
let promptSuffix = "";
|
|
352
410
|
// Posture is rendered once, then delivered through whichever channel this harness has: a
|
|
353
411
|
// claude lead gets it as --append-system-prompt-file below; a codex lead gets the identical
|
|
354
412
|
// text as config.toml's developer_instructions, built after ensureLeadRow (it needs leadActorId).
|
|
@@ -386,6 +444,9 @@ async function cmdLead(argv) {
|
|
|
386
444
|
leadCommand += ` ${leadHarness.briefDelivery.systemPromptArgs(posturePath).map(shellQuote).join(" ")}`;
|
|
387
445
|
console.log(`- profile: ${profile} (${postureSource} posture; see it with: hive posture)`);
|
|
388
446
|
}
|
|
447
|
+
if (leadHarness.initialPromptArgs && firstMessage !== "") {
|
|
448
|
+
promptSuffix = ` ${leadHarness.initialPromptArgs(firstMessage).map(shellQuote).join(" ")}`;
|
|
449
|
+
}
|
|
389
450
|
}
|
|
390
451
|
const windowName = project.name;
|
|
391
452
|
const { agentId: leadAgentId, actorId: leadActorId, previousTarget, previousSocket, previousPanePid, previousCommand, previousCodexHome, casExpected, } = ensureLeadRow(project, leadCommand);
|
|
@@ -430,8 +491,8 @@ async function cmdLead(argv) {
|
|
|
430
491
|
throw e;
|
|
431
492
|
}
|
|
432
493
|
leadCommand += ` ${homeArgs.map(shellQuote).join(" ")}`;
|
|
433
|
-
if (leadHarness.initialPromptArgs) {
|
|
434
|
-
|
|
494
|
+
if (leadHarness.initialPromptArgs && firstMessage !== "") {
|
|
495
|
+
promptSuffix = ` ${leadHarness.initialPromptArgs(firstMessage).map(shellQuote).join(" ")}`;
|
|
435
496
|
}
|
|
436
497
|
if (renderedPosture !== null) {
|
|
437
498
|
console.log(`- profile: ${profile} (${postureSource} posture; see it with: hive posture)`);
|
|
@@ -442,13 +503,15 @@ async function cmdLead(argv) {
|
|
|
442
503
|
}
|
|
443
504
|
const envFlags = buildEnvFlags({
|
|
444
505
|
...leadEnv,
|
|
506
|
+
[FIRST_MESSAGE_SHA_ENV]: promptSuffix !== "" ? firstMessageDigest(firstMessage) : "",
|
|
445
507
|
...(newCodexHomeKey ? { CODEX_HOME: codexHomeDir(newCodexHomeKey) } : {}),
|
|
446
508
|
});
|
|
509
|
+
const launchCommand = leadCommand + promptSuffix;
|
|
447
510
|
const { leadPane, leadWindow, createdPane } = withWindowClaim(() => {
|
|
448
511
|
let leadPane;
|
|
449
512
|
let leadWindow;
|
|
450
513
|
let createdPane;
|
|
451
|
-
const started = ensureSession(session, project.path, { envFlags, command:
|
|
514
|
+
const started = ensureSession(session, project.path, { envFlags, command: launchCommand });
|
|
452
515
|
if (started.created) {
|
|
453
516
|
const claimed = claimInitialWindow(started, windowName, project.id);
|
|
454
517
|
leadPane = claimed.pane;
|
|
@@ -467,13 +530,13 @@ async function cmdLead(argv) {
|
|
|
467
530
|
createdPane = false;
|
|
468
531
|
}
|
|
469
532
|
else if (!foundWindow) {
|
|
470
|
-
const fresh = createWindow(session, windowName, project.path, envFlags,
|
|
533
|
+
const fresh = createWindow(session, windowName, project.path, envFlags, launchCommand, project.id, detach);
|
|
471
534
|
leadPane = fresh.pane;
|
|
472
535
|
leadWindow = fresh.window;
|
|
473
536
|
createdPane = true;
|
|
474
537
|
}
|
|
475
538
|
else {
|
|
476
|
-
leadPane = tmux("split-window", "-P", "-F", "#{pane_id}", "-t", foundWindow, "-c", project.path, ...envFlags,
|
|
539
|
+
leadPane = tmux("split-window", ...(detach ? ["-d"] : []), "-P", "-F", "#{pane_id}", "-t", foundWindow, "-c", project.path, ...envFlags, launchCommand);
|
|
477
540
|
leadWindow = foundWindow;
|
|
478
541
|
createdPane = true;
|
|
479
542
|
}
|
|
@@ -495,18 +558,7 @@ async function cmdLead(argv) {
|
|
|
495
558
|
// also reaps a stale codex home left over from a lead config that has since switched away from
|
|
496
559
|
// codex, since createdPane's branch does not require leadHarness.needsHome to fire.
|
|
497
560
|
const recordedCodexHome = createdPane ? (newCodexHomeKey ?? "") : previousCodexHome;
|
|
498
|
-
const
|
|
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) {
|
|
561
|
+
const abandonClaim = () => {
|
|
510
562
|
if (createdPane) {
|
|
511
563
|
try {
|
|
512
564
|
waitForPaneEstablished(leadPane);
|
|
@@ -517,6 +569,31 @@ async function cmdLead(argv) {
|
|
|
517
569
|
}
|
|
518
570
|
if (newCodexHomeKey)
|
|
519
571
|
reapCodexHome(newCodexHomeKey);
|
|
572
|
+
};
|
|
573
|
+
let wonRace;
|
|
574
|
+
try {
|
|
575
|
+
wonRace = db.transaction(() => {
|
|
576
|
+
const updated = db
|
|
577
|
+
.prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ?, command = ?, codex_home = ? WHERE id = ? AND tmux_target = ? AND status = 'running'")
|
|
578
|
+
.run(leadPane, tmuxSocketPath(process.env.TMUX, process.env.TMUX_TMPDIR), panePid(leadPane), recordedCommand, recordedCodexHome, leadAgentId, casExpected).changes;
|
|
579
|
+
if (updated === 0)
|
|
580
|
+
return false;
|
|
581
|
+
db.prepare(`UPDATE wakes SET deliver_pane = ?, held_at = NULL, held_reason = NULL
|
|
582
|
+
WHERE ${ACTIVE_TIMER_WHERE} AND deliver_actor = ?
|
|
583
|
+
AND (? = 1 OR held_reason IS NULL OR held_reason NOT LIKE ?)`).run(leadPane, leadActorId, createdPane ? 1 : 0, `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}%`);
|
|
584
|
+
confirmQueenWrite("hive lead", project.id, { detach }, {
|
|
585
|
+
agent_id: leadAgentId,
|
|
586
|
+
disposition: createdPane ? "started" : "adopted",
|
|
587
|
+
}, auditIdentity);
|
|
588
|
+
return true;
|
|
589
|
+
})();
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
abandonClaim();
|
|
593
|
+
throw e;
|
|
594
|
+
}
|
|
595
|
+
if (!wonRace) {
|
|
596
|
+
abandonClaim();
|
|
520
597
|
throw new Error("Another `hive lead` won the race to record a live pane for this project's lead session (both saw the " +
|
|
521
598
|
"same dead pane and both tried to replace it, or this row was closed by another process mid-restart). " +
|
|
522
599
|
"Re-run `hive lead`; it will attach to the pane that invocation recorded.");
|
|
@@ -550,6 +627,11 @@ async function cmdLead(argv) {
|
|
|
550
627
|
console.error(registrationNotice);
|
|
551
628
|
registrationNotice = null;
|
|
552
629
|
}
|
|
630
|
+
if (detach) {
|
|
631
|
+
console.log(`LEAD_PANE=${leadPane}`);
|
|
632
|
+
console.log(`ATTACH_COMMAND=hive attach ${shellQuote(project.path)}`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
553
635
|
if (!noDashboard)
|
|
554
636
|
maybeOpenDashboard(project, !!config?.dashboard);
|
|
555
637
|
attach(session, project, leadWindow);
|
|
@@ -560,6 +642,60 @@ async function cmdLead(argv) {
|
|
|
560
642
|
throw e;
|
|
561
643
|
}
|
|
562
644
|
}
|
|
645
|
+
function publishQueenYml(home) {
|
|
646
|
+
const yml = join(home, "hive.yml");
|
|
647
|
+
const staged = join(home, `.hive.yml.${process.pid}.${randomUUID()}`);
|
|
648
|
+
writeFileSync(staged, `profile: ${QUEEN_PROFILE}\n`, { flag: "wx" });
|
|
649
|
+
try {
|
|
650
|
+
linkSync(staged, yml);
|
|
651
|
+
}
|
|
652
|
+
catch (e) {
|
|
653
|
+
if (e.code !== "EEXIST")
|
|
654
|
+
throw e;
|
|
655
|
+
}
|
|
656
|
+
finally {
|
|
657
|
+
unlinkSync(staged);
|
|
658
|
+
}
|
|
659
|
+
if (lstatSync(yml).isSymbolicLink()) {
|
|
660
|
+
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.`);
|
|
661
|
+
}
|
|
662
|
+
const { config } = loadProjectYml(home);
|
|
663
|
+
if (activeProfile(config) !== QUEEN_PROFILE) {
|
|
664
|
+
throw new Error(`hive queen: ${yml} already exists and does not select "profile: ${QUEEN_PROFILE}". hive will not ` +
|
|
665
|
+
"overwrite it. Add that line yourself, or move the file away and run hive queen again.");
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function cmdQueen(argv) {
|
|
669
|
+
const parsed = parseArgs(argv, { flags: ["--no-dashboard"] });
|
|
670
|
+
rejectUnknownFlags("queen", parsed, "--no-dashboard");
|
|
671
|
+
if (parsed.positional.length > 0) {
|
|
672
|
+
console.error(`hive queen: takes no path. The queen lives in the data dir (${queenHomeDir()}).`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
if (process.env.HIVE_PROJECT_LOCK === "1" || agentProjectPin() != null) {
|
|
676
|
+
throw new Error("hive queen: this session is locked to its own project (HIVE_PROJECT_LOCK=1), so it cannot create or " +
|
|
677
|
+
"start the queen. Run hive queen from your own terminal.");
|
|
678
|
+
}
|
|
679
|
+
const home = queenHomeDir();
|
|
680
|
+
let existing = null;
|
|
681
|
+
try {
|
|
682
|
+
existing = lstatSync(home);
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
existing = null;
|
|
686
|
+
}
|
|
687
|
+
if (existing?.isSymbolicLink() || (existing && !existing.isDirectory())) {
|
|
688
|
+
throw new Error(`hive queen: ${home} exists and is not a plain directory. The queen's home must be a real directory ` +
|
|
689
|
+
"inside the data dir; move it away and run hive queen again.");
|
|
690
|
+
}
|
|
691
|
+
mkdirSync(home, { recursive: true });
|
|
692
|
+
publishQueenYml(home);
|
|
693
|
+
const queen = addProject(home, "queen");
|
|
694
|
+
generateQueenDashboardNow();
|
|
695
|
+
if (!parsed.flags.has("--no-dashboard"))
|
|
696
|
+
maybeOpenQueenDashboard(queen);
|
|
697
|
+
await cmdLead([home, ...parsed.flags]);
|
|
698
|
+
}
|
|
563
699
|
const HIVE_YML_TEMPLATE = `# hive project config. Read by \`hive lead\` from the project root.
|
|
564
700
|
# Commands defined here run only after a one-time interactive approval,
|
|
565
701
|
# and re-require it whenever they change.
|
|
@@ -577,6 +713,7 @@ const HIVE_YML_TEMPLATE = `# hive project config. Read by \`hive lead\` from the
|
|
|
577
713
|
# agents: [claude, codex] # crew harness pool; the first entry is the default
|
|
578
714
|
|
|
579
715
|
# lead_branches: [main, master] # branches where a session gets hive's kickoff
|
|
716
|
+
# first_message: "..." # a lead's first turn; absent or "" sends none
|
|
580
717
|
|
|
581
718
|
# context_checkpoint_percent: null # unset means off; integer 1-100 to enable
|
|
582
719
|
|
|
@@ -778,6 +915,7 @@ async function cmdInit(argv) {
|
|
|
778
915
|
process.exit(1);
|
|
779
916
|
}
|
|
780
917
|
const existing = getProjectByPath(registration);
|
|
918
|
+
assertQueenCrossProjectWrite("hive init", existing?.id ?? null, {});
|
|
781
919
|
project = existing ?? addProject(registration);
|
|
782
920
|
newlyRegistered = existing == null;
|
|
783
921
|
}
|
|
@@ -894,7 +1032,7 @@ itself to this project before any real work runs.`);
|
|
|
894
1032
|
}
|
|
895
1033
|
const NO_RUNBOOK_PAD_MESSAGE = `is on "profile: none" but has no runbook pad. Create one with: hive init`;
|
|
896
1034
|
function cmdRunbook(path) {
|
|
897
|
-
const project =
|
|
1035
|
+
const project = resolveProjectForRead("runbook", path);
|
|
898
1036
|
const { config, warnings } = loadProjectYml(project.path);
|
|
899
1037
|
for (const w of warnings)
|
|
900
1038
|
console.log(`! ${w}`);
|
|
@@ -919,7 +1057,7 @@ function cmdRunbook(path) {
|
|
|
919
1057
|
process.stdout.write(withTrailingNewline(rendered));
|
|
920
1058
|
}
|
|
921
1059
|
function cmdPosture(path) {
|
|
922
|
-
const project =
|
|
1060
|
+
const project = resolveProjectForRead("posture", path);
|
|
923
1061
|
const { config, warnings } = loadProjectYml(project.path);
|
|
924
1062
|
for (const w of warnings)
|
|
925
1063
|
console.log(`! ${w}`);
|
|
@@ -1060,7 +1198,7 @@ function cmdProfile(argv) {
|
|
|
1060
1198
|
vars = mergedProjectVars(here ? loadProjectYml(here.path).config : null);
|
|
1061
1199
|
}
|
|
1062
1200
|
else {
|
|
1063
|
-
const project =
|
|
1201
|
+
const project = resolveProjectForRead("profile read");
|
|
1064
1202
|
const { config, warnings } = loadProjectYml(project.path);
|
|
1065
1203
|
for (const w of warnings)
|
|
1066
1204
|
console.log(`! ${w}`);
|
|
@@ -1107,6 +1245,22 @@ function maybeOpenDashboard(project, dashboardEnabled) {
|
|
|
1107
1245
|
return;
|
|
1108
1246
|
if (!dashboardFileContained(dashboardFile, project.path))
|
|
1109
1247
|
return;
|
|
1248
|
+
openDashboardOnce(project, dashboardFile);
|
|
1249
|
+
}
|
|
1250
|
+
function maybeOpenQueenDashboard(project) {
|
|
1251
|
+
if (process.platform !== "darwin")
|
|
1252
|
+
return;
|
|
1253
|
+
const dashboardFile = join(project.path, "dashboard.html");
|
|
1254
|
+
try {
|
|
1255
|
+
if (!existsSync(dashboardFile) || !dashboardFileContained(dashboardFile, project.path))
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
catch {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
openDashboardOnce(project, dashboardFile);
|
|
1262
|
+
}
|
|
1263
|
+
function openDashboardOnce(project, dashboardFile) {
|
|
1110
1264
|
const actor = currentActor();
|
|
1111
1265
|
const claim = db
|
|
1112
1266
|
.prepare(`INSERT INTO kv (project_id, key, value, updated_by, expires_at)
|
|
@@ -1132,7 +1286,7 @@ function cmdAttach(argv) {
|
|
|
1132
1286
|
process.exit(1);
|
|
1133
1287
|
}
|
|
1134
1288
|
const path = argv.find((a) => !a.startsWith("--"));
|
|
1135
|
-
const project =
|
|
1289
|
+
const project = resolveProjectForWrite("hive attach", path);
|
|
1136
1290
|
maybeOpenDashboard(project, !!loadProjectYml(project.path).config?.dashboard);
|
|
1137
1291
|
const session = sessionName();
|
|
1138
1292
|
const window = withWindowClaim(() => {
|
|
@@ -1160,7 +1314,7 @@ async function cmdStart(argv) {
|
|
|
1160
1314
|
console.log("Usage: hive start <process> [path]");
|
|
1161
1315
|
process.exit(1);
|
|
1162
1316
|
}
|
|
1163
|
-
const project =
|
|
1317
|
+
const project = resolveProjectForWrite("hive start", path);
|
|
1164
1318
|
const { config, warnings } = loadProjectYml(project.path);
|
|
1165
1319
|
for (const w of warnings)
|
|
1166
1320
|
console.log(`! ${w}`);
|
|
@@ -1188,7 +1342,7 @@ function resolveNamedProcess(argv, verb) {
|
|
|
1188
1342
|
console.log(`Usage: hive ${verb} <process> [path]`);
|
|
1189
1343
|
process.exit(1);
|
|
1190
1344
|
}
|
|
1191
|
-
const project =
|
|
1345
|
+
const project = resolveProjectForWrite(`hive ${verb}`, parsed.positional[1]);
|
|
1192
1346
|
const { config, warnings } = loadProjectYml(project.path);
|
|
1193
1347
|
for (const w of warnings)
|
|
1194
1348
|
console.log(`! ${w}`);
|
|
@@ -1228,7 +1382,7 @@ function leadPaneExitedHookCommand() {
|
|
|
1228
1382
|
// started from inside a worker's pane hands them to every run-shell it ever runs, where a stale
|
|
1229
1383
|
// HIVE_AGENT_ID is a hard refusal before the verb is reached at all. The same clearing
|
|
1230
1384
|
// 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"]
|
|
1385
|
+
const clear = ["HIVE_AGENT_ID", "HIVE_AGENT_NAME", "HIVE_LEAD", FIRST_MESSAGE_SHA_ENV, "HIVE_PROJECT_LOCK", "HIVE_PROJECT_PATH"]
|
|
1232
1386
|
.flatMap((name) => ["-u", name]);
|
|
1233
1387
|
// Redirected because tmux prints a run-shell's output into the pane it fires from, which is a
|
|
1234
1388
|
// human's own window: a backstop that failed to start must not type into it.
|
|
@@ -1310,7 +1464,7 @@ function cmdStop(argv) {
|
|
|
1310
1464
|
const parsed = parseArgs(argv, { flags: ["--all"] });
|
|
1311
1465
|
rejectUnknownFlags("stop", parsed, "--all");
|
|
1312
1466
|
if (parsed.flags.has("--all")) {
|
|
1313
|
-
const project =
|
|
1467
|
+
const project = resolveProjectForWrite("hive stop", parsed.positional[0]);
|
|
1314
1468
|
const stopped = stopAllProcesses(project.id, STOP_REASONS.byHand);
|
|
1315
1469
|
console.log(stopped.length === 0 ? "No processes are running." : stopped.map(stopLine).join("\n"));
|
|
1316
1470
|
return;
|
|
@@ -1320,7 +1474,7 @@ function cmdStop(argv) {
|
|
|
1320
1474
|
console.log("Usage: hive stop <process> [path] | hive stop --all [path]");
|
|
1321
1475
|
process.exit(1);
|
|
1322
1476
|
}
|
|
1323
|
-
const project =
|
|
1477
|
+
const project = resolveProjectForWrite("hive stop", parsed.positional[1]);
|
|
1324
1478
|
const row = runningCommandRow(project.id, name);
|
|
1325
1479
|
if (!row) {
|
|
1326
1480
|
const { config, warnings } = loadProjectYml(project.path);
|
|
@@ -1723,7 +1877,7 @@ function reportProjectScope(here) {
|
|
|
1723
1877
|
if (locked && project.id !== here?.id)
|
|
1724
1878
|
continue;
|
|
1725
1879
|
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.
|
|
1880
|
+
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 any hive session) removes it once it holds nothing.`);
|
|
1727
1881
|
}
|
|
1728
1882
|
}
|
|
1729
1883
|
for (const parent of projects) {
|
|
@@ -2595,6 +2749,144 @@ function findPadExports(projectId, name) {
|
|
|
2595
2749
|
.filter((f) => f.startsWith(prefix) && f.endsWith(".md"))
|
|
2596
2750
|
.map((f) => join(tmpdir(), f));
|
|
2597
2751
|
}
|
|
2752
|
+
function canonicalPath(path) {
|
|
2753
|
+
try {
|
|
2754
|
+
return realpathSync(path);
|
|
2755
|
+
}
|
|
2756
|
+
catch {
|
|
2757
|
+
return resolve(path);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
function nextCandidates() {
|
|
2761
|
+
const queenRoot = canonicalPath(join(storeDir(), "queen"));
|
|
2762
|
+
const byCount = (a, b) => b - a;
|
|
2763
|
+
return collectPortfolio()
|
|
2764
|
+
.projects.filter((p) => (p.lane === "waiting_on_you" || p.lane === "stuck" || (p.lane === "moving" && p.needs_human > 0)) &&
|
|
2765
|
+
canonicalPath(p.root) !== queenRoot)
|
|
2766
|
+
.sort((a, b) => {
|
|
2767
|
+
const tier = (p) => (p.lane === "waiting_on_you" ? 0 : p.needs_human > 0 ? 1 : 2);
|
|
2768
|
+
if (tier(a) !== tier(b))
|
|
2769
|
+
return tier(a) - tier(b);
|
|
2770
|
+
if (a.lane === "waiting_on_you") {
|
|
2771
|
+
return (byCount(a.needs_human, b.needs_human) ||
|
|
2772
|
+
a.last_activity_at.localeCompare(b.last_activity_at) ||
|
|
2773
|
+
a.id - b.id);
|
|
2774
|
+
}
|
|
2775
|
+
return (byCount(a.needs_human, b.needs_human) ||
|
|
2776
|
+
byCount(a.todos.blocked_in_progress, b.todos.blocked_in_progress) ||
|
|
2777
|
+
byCount(a.wakes.overdue, b.wakes.overdue) ||
|
|
2778
|
+
byCount(a.workers.needs_input, b.workers.needs_input) ||
|
|
2779
|
+
a.last_activity_at.localeCompare(b.last_activity_at) ||
|
|
2780
|
+
a.id - b.id);
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
function collectProjectById(id) {
|
|
2784
|
+
return collectPortfolio().projects.find((p) => p.id === id);
|
|
2785
|
+
}
|
|
2786
|
+
async function cmdNext(argv) {
|
|
2787
|
+
const parsed = parseArgs(argv, { flags: ["--print"] });
|
|
2788
|
+
if (parsed.positional.length > 0)
|
|
2789
|
+
parsed.unknown.push(parsed.positional[0]);
|
|
2790
|
+
rejectUnknownFlags("next", parsed, "--print");
|
|
2791
|
+
if (process.env.HIVE_PROJECT_LOCK === "1") {
|
|
2792
|
+
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.");
|
|
2793
|
+
process.exit(1);
|
|
2794
|
+
}
|
|
2795
|
+
const chosen = nextCandidates()[0];
|
|
2796
|
+
if (!chosen) {
|
|
2797
|
+
console.log("No project needs you: none is waiting, stuck, or holding a needs-human item.");
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
console.log(JSON.stringify({
|
|
2801
|
+
project_id: chosen.id,
|
|
2802
|
+
name: chosen.name,
|
|
2803
|
+
root: chosen.root,
|
|
2804
|
+
lane: chosen.lane,
|
|
2805
|
+
reasons: chosen.reasons,
|
|
2806
|
+
lead_state: chosen.lead.state,
|
|
2807
|
+
}));
|
|
2808
|
+
if (parsed.flags.has("--print"))
|
|
2809
|
+
return;
|
|
2810
|
+
assertQueenCrossProjectWrite("hive next", chosen.id, {});
|
|
2811
|
+
if (!chosen.root_exists) {
|
|
2812
|
+
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.`);
|
|
2813
|
+
process.exit(1);
|
|
2814
|
+
}
|
|
2815
|
+
let state = collectProjectById(chosen.id)?.lead.state ?? "none";
|
|
2816
|
+
if (state === "unknown") {
|
|
2817
|
+
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.`);
|
|
2818
|
+
process.exit(1);
|
|
2819
|
+
}
|
|
2820
|
+
await cmdLead([chosen.root, "--detach"]);
|
|
2821
|
+
state = collectProjectById(chosen.id)?.lead.state ?? "none";
|
|
2822
|
+
if (state !== "alive" || (process.exitCode !== undefined && process.exitCode !== 0)) {
|
|
2823
|
+
console.error(`hive next: the lead for project ${chosen.id} ("${chosen.name}") did not start cleanly (${state}). Not attaching.`);
|
|
2824
|
+
process.exit(1);
|
|
2825
|
+
}
|
|
2826
|
+
cmdAttach([chosen.root]);
|
|
2827
|
+
}
|
|
2828
|
+
function cmdQueenAudit(argv) {
|
|
2829
|
+
const parsed = parseArgs(argv, { flags: ["--json"], valued: ["--project-id", "--limit"] });
|
|
2830
|
+
if (parsed.positional.length > 0)
|
|
2831
|
+
parsed.unknown.push(parsed.positional[0]);
|
|
2832
|
+
rejectUnknownFlags("queen-audit", parsed, "--project-id <id>, --limit <n>, --json");
|
|
2833
|
+
requireFlagValues("queen-audit", parsed);
|
|
2834
|
+
let report;
|
|
2835
|
+
try {
|
|
2836
|
+
const project = pinnedOrCwdProject();
|
|
2837
|
+
if (!project)
|
|
2838
|
+
throw new Error("hive queen-audit: the cwd is not a registered project. Run this command from a registered project.");
|
|
2839
|
+
report = listQueenAudit({
|
|
2840
|
+
project_id: parsed.values.has("--project-id") ? Number(parsed.values.get("--project-id")) : undefined,
|
|
2841
|
+
limit: parsed.values.has("--limit") ? Number(parsed.values.get("--limit")) : undefined,
|
|
2842
|
+
}, project.id);
|
|
2843
|
+
}
|
|
2844
|
+
catch (e) {
|
|
2845
|
+
console.error(errorMessage(e));
|
|
2846
|
+
process.exitCode = 1;
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
if (parsed.flags.has("--json")) {
|
|
2850
|
+
console.log(JSON.stringify(report));
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
if (report.entries.length === 0) {
|
|
2854
|
+
console.log("No queen actions recorded.");
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
for (const entry of report.entries) {
|
|
2858
|
+
console.log(`${entry.created_at} UTC ${entry.actor_id} -> target #${entry.target_project_id}\n` +
|
|
2859
|
+
` ${entry.operation} ${entry.resource_type} #${entry.resource_id}\n ${entry.summary}`);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
function cmdPortfolio(argv) {
|
|
2863
|
+
const parsed = parseArgs(argv, { flags: ["--json"] });
|
|
2864
|
+
if (parsed.positional.length > 0)
|
|
2865
|
+
parsed.unknown.push(parsed.positional[0]);
|
|
2866
|
+
rejectUnknownFlags("portfolio", parsed, "--json");
|
|
2867
|
+
const report = collectPortfolio();
|
|
2868
|
+
if (parsed.flags.has("--json")) {
|
|
2869
|
+
console.log(JSON.stringify(report));
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
if (report.projects.length === 0) {
|
|
2873
|
+
console.log("No registered projects.");
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
for (const p of report.projects) {
|
|
2877
|
+
const w = p.workers;
|
|
2878
|
+
const t = p.todos;
|
|
2879
|
+
console.log(`${p.lane.padEnd(14)} ${p.name} (#${p.id})${p.root_exists ? "" : " [root missing]"}\n` +
|
|
2880
|
+
` why: ${p.reasons.join(", ")}\n` +
|
|
2881
|
+
` lead ${leadText(p)}; workers ${w.working} working, ${w.idle} idle, ${w.needs_input} needs input, ` +
|
|
2882
|
+
`${w.other} other, ${w.unreachable} unreachable, ${w.unconfirmed} unconfirmed\n` +
|
|
2883
|
+
` todos ${t.open} open, ${t.in_progress} in progress, ${t.blocked} blocked, ${t.high} high; ` +
|
|
2884
|
+
`needs human ${p.needs_human}; wakes ${p.wakes.pending} pending, ${p.wakes.overdue} overdue\n` +
|
|
2885
|
+
` last activity ${p.last_activity_at} UTC`);
|
|
2886
|
+
}
|
|
2887
|
+
const l = report.totals.lanes;
|
|
2888
|
+
console.log(`\n${report.totals.projects} project(s): ${l.waiting_on_you} waiting on you, ${l.stuck} stuck, ${l.moving} moving, ${l.quiet} quiet`);
|
|
2889
|
+
}
|
|
2598
2890
|
function cmdBackups() {
|
|
2599
2891
|
const snapshots = listSnapshots(dataDir);
|
|
2600
2892
|
if (snapshots.length === 0) {
|
|
@@ -2762,9 +3054,7 @@ function requireFlagValues(command, parsed) {
|
|
|
2762
3054
|
process.exit(1);
|
|
2763
3055
|
}
|
|
2764
3056
|
function cmdTodos(argv) {
|
|
2765
|
-
const project =
|
|
2766
|
-
if (!project)
|
|
2767
|
-
return;
|
|
3057
|
+
const project = resolveProjectForRead("todos");
|
|
2768
3058
|
const all = argv.includes("--all");
|
|
2769
3059
|
const statusIdx = argv.indexOf("--status");
|
|
2770
3060
|
const status = flagValue(argv, "--status");
|
|
@@ -2808,9 +3098,7 @@ function cmdTodos(argv) {
|
|
|
2808
3098
|
}
|
|
2809
3099
|
}
|
|
2810
3100
|
function cmdTodo(argv) {
|
|
2811
|
-
const project =
|
|
2812
|
-
if (!project)
|
|
2813
|
-
return;
|
|
3101
|
+
const project = resolveProjectForRead("todo");
|
|
2814
3102
|
const id = Number(argv.find((a) => !a.startsWith("--")));
|
|
2815
3103
|
if (!Number.isInteger(id)) {
|
|
2816
3104
|
console.log("Usage: hive todo <id> (run inside the project)");
|
|
@@ -2851,7 +3139,7 @@ function cmdTodo(argv) {
|
|
|
2851
3139
|
}
|
|
2852
3140
|
}
|
|
2853
3141
|
function cmdPads() {
|
|
2854
|
-
const project =
|
|
3142
|
+
const project = resolveProjectForRead("pads");
|
|
2855
3143
|
const pads = listActivePads(project.id);
|
|
2856
3144
|
if (pads.length === 0) {
|
|
2857
3145
|
console.log(`No pads in project "${project.name}".`);
|
|
@@ -2870,7 +3158,7 @@ function cmdPad(argv) {
|
|
|
2870
3158
|
console.log("Usage: hive pad <name> [--edit | --save [file]] (run inside the project)");
|
|
2871
3159
|
process.exit(1);
|
|
2872
3160
|
}
|
|
2873
|
-
const project =
|
|
3161
|
+
const project = parsed.flags.has("--save") ? resolveProjectForWrite("hive pad --save") : resolveProjectForRead("pad");
|
|
2874
3162
|
const pad = getActivePadByName(project.id, name);
|
|
2875
3163
|
if (!pad) {
|
|
2876
3164
|
console.log(`No pad named "${name}" in project "${project.name}". List them with: hive pads`);
|
|
@@ -2940,6 +3228,11 @@ function cmdPad(argv) {
|
|
|
2940
3228
|
}
|
|
2941
3229
|
process.stdout.write(withTrailingNewline(pad.content));
|
|
2942
3230
|
}
|
|
3231
|
+
process.stdout.on("error", (error) => {
|
|
3232
|
+
if (error.code === "EPIPE")
|
|
3233
|
+
process.exit(0);
|
|
3234
|
+
throw error;
|
|
3235
|
+
});
|
|
2943
3236
|
const args = process.argv.slice(2);
|
|
2944
3237
|
let command = args[0] ?? "lead";
|
|
2945
3238
|
let rest = args.slice(1);
|
|
@@ -2964,7 +3257,7 @@ if (command === "--version" || command === "-v") {
|
|
|
2964
3257
|
process.exit(0);
|
|
2965
3258
|
}
|
|
2966
3259
|
const COMMANDS = [
|
|
2967
|
-
"lead", "init", "attach", "start", "stop", "show", "hide", "status", "setup", "upgrade", "doctor",
|
|
3260
|
+
"lead", "queen", "queen-audit", "init", "attach", "start", "stop", "show", "hide", "status", "portfolio", "next", "setup", "upgrade", "doctor",
|
|
2968
3261
|
LEAD_PANE_EXITED_VERB,
|
|
2969
3262
|
"pads", "pad", "todos", "todo", "backups", "restore", "runbook", "posture", "profile", "kickoff", "statusline",
|
|
2970
3263
|
];
|
|
@@ -2988,6 +3281,9 @@ try {
|
|
|
2988
3281
|
case "lead":
|
|
2989
3282
|
await cmdLead(rest);
|
|
2990
3283
|
break;
|
|
3284
|
+
case "queen":
|
|
3285
|
+
await cmdQueen(rest);
|
|
3286
|
+
break;
|
|
2991
3287
|
case "init":
|
|
2992
3288
|
await cmdInit(rest);
|
|
2993
3289
|
break;
|
|
@@ -3012,6 +3308,15 @@ try {
|
|
|
3012
3308
|
case "status":
|
|
3013
3309
|
cmdStatus();
|
|
3014
3310
|
break;
|
|
3311
|
+
case "portfolio":
|
|
3312
|
+
cmdPortfolio(rest);
|
|
3313
|
+
break;
|
|
3314
|
+
case "queen-audit":
|
|
3315
|
+
cmdQueenAudit(rest);
|
|
3316
|
+
break;
|
|
3317
|
+
case "next":
|
|
3318
|
+
await cmdNext(rest);
|
|
3319
|
+
break;
|
|
3015
3320
|
case "setup":
|
|
3016
3321
|
cmdSetup(rest);
|
|
3017
3322
|
break;
|