@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/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 +419 -56
- package/dist/context.js +193 -2
- package/dist/db.js +61 -18
- package/dist/firstMessage.js +5 -0
- package/dist/harnesses.js +6 -4
- package/dist/help.js +32 -11
- 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/projectRemove.js +68 -0
- package/dist/projectYml.js +19 -0
- package/dist/queenAudit.js +132 -0
- package/dist/queenDashboard.js +573 -0
- package/dist/scheduler.js +179 -15
- package/dist/strictInput.js +24 -7
- package/dist/tmux.js +82 -6
- package/dist/tools/agents.js +4 -0
- package/dist/tools/leases.js +2 -2
- package/dist/tools/meta.js +51 -19
- 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,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 {
|
|
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
|
|
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
|
-
|
|
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}".
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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;
|