@cmgmyr/hive 1.10.1 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/claude-plugin/crew/model.mjs +2 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +135 -52
- package/dist/context.js +49 -5
- package/dist/crew.js +20 -2
- package/dist/dashboard.js +52 -0
- package/dist/db.js +144 -0
- package/dist/help.js +27 -0
- package/dist/hook.js +32 -2
- package/dist/index.js +2 -0
- package/dist/kickoff.js +29 -6
- package/dist/leadHandoff.js +525 -0
- package/dist/leadHandoffCli.js +295 -0
- package/dist/leadHandoffState.js +96 -0
- package/dist/leadLaunch.js +54 -0
- package/dist/leadMessageDelivery.js +44 -7
- package/dist/leadState.js +98 -17
- package/dist/portfolio.js +7 -1
- package/dist/projectRemove.js +1 -0
- package/dist/projectYml.js +8 -1
- package/dist/queenAudit.js +14 -2
- package/dist/queenDashboard.js +14 -8
- package/dist/questions.js +279 -0
- package/dist/scheduler.js +25 -5
- package/dist/staleServers.js +135 -0
- package/dist/tools/agents.js +2 -2
- package/dist/tools/meta.js +4 -1
- package/dist/tools/queenAudit.js +1 -1
- package/dist/tools/questions.js +88 -0
- package/dist/tools/wakes.js +25 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ hive is a personal daily-driver tool, released low-key. It is single-user by des
|
|
|
70
70
|
| [Dashboard](docs/dashboard.md) | The generated dashboard: enabling it, where it lives, what it shows |
|
|
71
71
|
| [Install details](docs/install.md) | The interpreter pin, iTerm settings, the status line, MCP scope, codex workers, updating, uninstalling |
|
|
72
72
|
| [Troubleshooting](docs/troubleshooting.md) | Common errors and their fixes |
|
|
73
|
+
| [Security](docs/security.md) | What hive runs, writes and fetches on your machine, with the code to check |
|
|
73
74
|
| [Tools](docs/tools.md) | The MCP tools: what each does and when to use it |
|
|
74
75
|
| [tmux settings](docs/tmux.md) | Attach modes, pane options, and what to put in `~/.tmux.conf` |
|
|
75
76
|
| [Development](docs/development.md) | Building and testing hive itself |
|
|
@@ -121,6 +121,8 @@ export function buildCrewView(snapshot, previousView, nowMs) {
|
|
|
121
121
|
const { wakes } = snapshot;
|
|
122
122
|
const workers = snapshot.lanes.filter((l) => l.worker).length;
|
|
123
123
|
const footer = [{ text: nextLine(wakes.next, nowMs) }];
|
|
124
|
+
const questionsPending = snapshot.questions?.pending ?? 0;
|
|
125
|
+
if (questionsPending > 0) footer.push({ text: `questions pending: ${questionsPending}` });
|
|
124
126
|
if (wakes.watching.length > 0) footer.push({ text: `watching: ${wakes.watching.map(watchText).join(", ")}` });
|
|
125
127
|
else if (workers > 0 && !covered(snapshot)) footer.push({ text: "unwatched: no standing watch on running workers", amber: true });
|
|
126
128
|
return {
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.
|
|
1
|
+
{"version":"1.11.1","sha":"59218ca","dirty":false,"build_id":"caa5cf415058468f8a1cd7152eac5af2255c5aed0e53f33e637bd75e3200fda0"}
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ 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 { staleHiveServers } from "./staleServers.js";
|
|
20
21
|
import { addProject, agentProjectPin, assertQueenCrossProjectWrite, captureQueenWriteIdentity, currentActor, effectiveProjectId, findProjectForCwd, findProjectForDir, getProjectByPath, gitPrimaryRoot, getProject, listProjects, QUEEN_PROFILE, queenHomeDir, registeredAncestor, takeRegistrationNotice, } from "./context.js";
|
|
21
22
|
import { confirmQueenWrite, listQueenAudit } from "./queenAudit.js";
|
|
22
23
|
import { formatRowCounts, projectRowCounts, removeProject } from "./projectRemove.js";
|
|
@@ -37,8 +38,10 @@ import { collectCrew, crewStoreProblem } from "./crew.js";
|
|
|
37
38
|
import { describeVisibility, formatLocal, processCounts } from "./dashboard.js";
|
|
38
39
|
import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, STOP_REASONS, } from "./processes.js";
|
|
39
40
|
import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
41
|
+
import { appendClaudeLeadArgs, crewPluginDir, isTrusted, leadIdentityEnv, renderLeadPosture } from "./leadLaunch.js";
|
|
42
|
+
import { handoffStatusSegment } from "./leadHandoff.js";
|
|
43
|
+
import { cmdLeadHandoff, runHandoffGrace, runHandoffStart } from "./leadHandoffCli.js";
|
|
44
|
+
import { harnessFor, hasTranscriptSignal, paneClassifierFor, transcriptDirFor } from "./harnesses.js";
|
|
42
45
|
import { codexHomeDir, codexInstructionsPhrase, ensureCodexHome, reapCodexHome } from "./codexHome.js";
|
|
43
46
|
import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
|
|
44
47
|
import { ageSecondsSince, deriveProvenance, describeForHuman, describeLastLogEvent, humanizeAge, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "./stateProvenance.js";
|
|
@@ -46,6 +49,7 @@ import { awaitingFirstPromptSql } from "./firstPrompt.js";
|
|
|
46
49
|
import { checkoutRoot, createProfile, forkProfile, PROFILE_FILES, ProfileError, profileExists, profileFileNames, profileNames, profileStatus, readProfileFile, referencedPads, referencedPaths, renderProfileFile, resolveProfileFile, REWRITE_THRESHOLD, templateVars, userProfilesDir, } from "./profiles.js";
|
|
47
50
|
import { getTodoDetail, listTodoSummaries, OPEN_BLOCKERS_SQL, TODO_STATUSES, } from "./tools/todos.js";
|
|
48
51
|
import { createPad, getActivePadByName, listActivePads, overwritePadContent, } from "./tools/pads.js";
|
|
52
|
+
import { answerQuestion, listQuestions } from "./questions.js";
|
|
49
53
|
function usage() {
|
|
50
54
|
console.log(`hive — run a crew of Claude Code and Codex workers from one lead session
|
|
51
55
|
|
|
@@ -74,7 +78,7 @@ Usage:
|
|
|
74
78
|
snapshot; read-only, writes nothing
|
|
75
79
|
hive portfolio [--json] one row per registered project: lane, lead, workers,
|
|
76
80
|
todos, needs-human count, wakes; read-only
|
|
77
|
-
hive queen-audit
|
|
81
|
+
hive queen-audit queen writes and refused calls, newest first; 30-day history
|
|
78
82
|
[--project-id <id>] filter the target project (queen only across projects)
|
|
79
83
|
[--limit <n>] [--json] default 20, maximum 100; JSON prints one object
|
|
80
84
|
hive next [--print] attach to the lead of the project that needs you most
|
|
@@ -99,6 +103,10 @@ Usage:
|
|
|
99
103
|
hive todos [--all] [--status <s>] [--tag <t>]
|
|
100
104
|
list this project's todos; open work by default
|
|
101
105
|
hive todo <id> print one todo in full, comments included
|
|
106
|
+
hive questions [--json] list pending and defaulted questions
|
|
107
|
+
hive answer <id> <answer> [--notes <text>]
|
|
108
|
+
record an answer to a pending question
|
|
109
|
+
use -- before an answer that starts with a dash
|
|
102
110
|
hive backups list automatic store snapshots
|
|
103
111
|
hive restore <name> [--yes] [--force] overwrite the live store from a snapshot
|
|
104
112
|
hive project rm <id|path> [--yes]
|
|
@@ -109,6 +117,7 @@ Usage:
|
|
|
109
117
|
hive kickoff [--explain] SessionStart hook output; silent unless this is a lead checkout
|
|
110
118
|
hive profile [list|path|fork|create|read] standing instructions shared across projects
|
|
111
119
|
hive statusline one-line store summary; silent outside hive projects
|
|
120
|
+
hive lead-handoff a lead confirms its persisted handoff pad (run from a handoff request)
|
|
112
121
|
|
|
113
122
|
hive lead reads hive.yml from the project root when present; hive init
|
|
114
123
|
writes this starter file (uncomment what you need):
|
|
@@ -204,12 +213,6 @@ async function ensureTrusted(projectId, name, command, dir, env) {
|
|
|
204
213
|
db.prepare("INSERT OR IGNORE INTO command_trust (project_id, name, config_hash) VALUES (?, ?, ?)").run(projectId, name, hash);
|
|
205
214
|
return true;
|
|
206
215
|
}
|
|
207
|
-
function isTrusted(projectId, name, command, dir, env) {
|
|
208
|
-
const hash = configHash(name, command, dir, env);
|
|
209
|
-
return !!db
|
|
210
|
-
.prepare("SELECT 1 FROM command_trust WHERE project_id = ? AND name = ? AND config_hash = ?")
|
|
211
|
-
.get(projectId, name, hash);
|
|
212
|
-
}
|
|
213
216
|
function startYmlCommand(project, name, proc, config) {
|
|
214
217
|
if (isReservedAgentName(name)) {
|
|
215
218
|
return `skipped: "lead" is reserved for this project's lead session and cannot be used as a process name`;
|
|
@@ -426,18 +429,9 @@ async function cmdLead(argv) {
|
|
|
426
429
|
// Posture is rendered once, then delivered through whichever channel this harness has: a
|
|
427
430
|
// claude lead gets it as --append-system-prompt-file below; a codex lead gets the identical
|
|
428
431
|
// text as config.toml's developer_instructions, built after ensureLeadRow (it needs leadActorId).
|
|
429
|
-
const profile =
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (profile) {
|
|
433
|
-
const posture = resolveProfileFile(profile, "posture.md");
|
|
434
|
-
if (!posture) {
|
|
435
|
-
console.log(`! profile "${profile}" has no posture.md on this machine; starting without it.`);
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
renderedPosture = renderProfileFile(profile, "posture.md", mergedProjectVars(config)) ?? "";
|
|
439
|
-
postureSource = posture.source;
|
|
440
|
-
}
|
|
432
|
+
const { profile, rendered: renderedPosture, source: postureSource } = renderLeadPosture(config);
|
|
433
|
+
if (profile && renderedPosture === null) {
|
|
434
|
+
console.log(`! profile "${profile}" has no posture.md on this machine; starting without it.`);
|
|
441
435
|
}
|
|
442
436
|
if (leadHarness.needsHome) {
|
|
443
437
|
// Wired below, once leadActorId is known - a codex lead's hooks/posture/triage all live in
|
|
@@ -451,21 +445,19 @@ async function cmdLead(argv) {
|
|
|
451
445
|
}
|
|
452
446
|
}
|
|
453
447
|
else {
|
|
454
|
-
|
|
455
|
-
leadCommand
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
448
|
+
({ command: leadCommand, promptSuffix } = appendClaudeLeadArgs({
|
|
449
|
+
command: leadCommand,
|
|
450
|
+
harness: leadHarness,
|
|
451
|
+
projectId: project.id,
|
|
452
|
+
projectName: project.name,
|
|
453
|
+
hooksPath,
|
|
454
|
+
sidebarPluginDir: config?.lead_sidebar === true && leadHarness.name === "claude" ? crewPluginDir() : null,
|
|
455
|
+
posture: renderedPosture,
|
|
456
|
+
firstMessage,
|
|
457
|
+
}));
|
|
461
458
|
if (renderedPosture !== null) {
|
|
462
|
-
const posturePath = writeProjectPosture(project.id, renderedPosture);
|
|
463
|
-
leadCommand += ` ${leadHarness.briefDelivery.systemPromptArgs(posturePath).map(shellQuote).join(" ")}`;
|
|
464
459
|
console.log(`- profile: ${profile} (${postureSource} posture; see it with: hive posture)`);
|
|
465
460
|
}
|
|
466
|
-
if (leadHarness.initialPromptArgs && firstMessage !== "") {
|
|
467
|
-
promptSuffix = ` ${leadHarness.initialPromptArgs(firstMessage).map(shellQuote).join(" ")}`;
|
|
468
|
-
}
|
|
469
461
|
}
|
|
470
462
|
const windowName = project.name;
|
|
471
463
|
const { agentId: leadAgentId, actorId: leadActorId, previousTarget, previousSocket, previousPanePid, previousCommand, previousCodexHome, casExpected, casSocket, casPid, } = ensureLeadRow(project, leadCommand);
|
|
@@ -475,14 +467,7 @@ async function cmdLead(argv) {
|
|
|
475
467
|
// actually starts under it) and correct regardless of whether this invocation ends up creating
|
|
476
468
|
// a pane or adopting one. Which home ends up orphaned - this one, or the previous invocation's -
|
|
477
469
|
// is decided after createdPane is known, below.
|
|
478
|
-
const leadEnv =
|
|
479
|
-
HIVE_AGENT_ID: leadActorId,
|
|
480
|
-
HIVE_AGENT_NAME: LEAD_NAME,
|
|
481
|
-
HIVE_LEAD: "1",
|
|
482
|
-
HIVE_DATA_DIR: dataDir,
|
|
483
|
-
HIVE_PROJECT_LOCK: "",
|
|
484
|
-
HIVE_PROJECT_PATH: "",
|
|
485
|
-
};
|
|
470
|
+
const leadEnv = leadIdentityEnv(leadActorId);
|
|
486
471
|
let newCodexHomeKey;
|
|
487
472
|
if (leadHarness.needsHome) {
|
|
488
473
|
newCodexHomeKey = randomUUID();
|
|
@@ -1288,9 +1273,6 @@ function cmdProfile(argv) {
|
|
|
1288
1273
|
throw e;
|
|
1289
1274
|
}
|
|
1290
1275
|
}
|
|
1291
|
-
function crewPluginDir() {
|
|
1292
|
-
return join(checkoutRoot, "claude-plugin", "crew");
|
|
1293
|
-
}
|
|
1294
1276
|
const DASHBOARD_OPENED_KV_KEY = "hive:dashboard_opened";
|
|
1295
1277
|
const DASHBOARD_OPENED_TTL_SECONDS = 8 * 60 * 60;
|
|
1296
1278
|
function maybeOpenDashboard(project, dashboardEnabled) {
|
|
@@ -1436,6 +1418,8 @@ function resolveNamedProcess(argv, verb) {
|
|
|
1436
1418
|
}
|
|
1437
1419
|
const LEAD_PANE_EXITED_HOOK = "pane-exited";
|
|
1438
1420
|
const LEAD_PANE_EXITED_VERB = "lead-pane-exited";
|
|
1421
|
+
const LEAD_HANDOFF_GRACE_VERB = "lead-handoff-grace";
|
|
1422
|
+
const LEAD_HANDOFF_START_VERB = "lead-handoff-start";
|
|
1439
1423
|
// tmux runs a hook with the SERVER's environment and no login shell, so a bare `hive` resolves to
|
|
1440
1424
|
// nothing (the iTerm rule in .claude/rules/tmux-and-panes.md) and HIVE_DATA_DIR is not there at all
|
|
1441
1425
|
// - without it the backstop would open the DEFAULT store and find no project. Absolute interpreter,
|
|
@@ -2364,6 +2348,18 @@ function cmdDoctor(argv) {
|
|
|
2364
2348
|
}
|
|
2365
2349
|
return `${dataDir} (schema v${n})`;
|
|
2366
2350
|
});
|
|
2351
|
+
check("stale hive servers", () => {
|
|
2352
|
+
const { applied_at: appliedAt } = db.prepare("SELECT MAX(applied_at) AS applied_at FROM migrations").get();
|
|
2353
|
+
const result = staleHiveServers(appliedAt, dataDir);
|
|
2354
|
+
if ("skipped" in result) {
|
|
2355
|
+
info("stale hive servers", `check skipped: ${result.skipped}`);
|
|
2356
|
+
return "check skipped";
|
|
2357
|
+
}
|
|
2358
|
+
for (const server of result.servers) {
|
|
2359
|
+
warn("stale hive server", `pid ${server.pid} started ${server.startedAt} (parent pid ${server.ppid}: ${server.parentCommand ?? "unknown"})`, `command: ${server.command}`, "restart that session to load the current build");
|
|
2360
|
+
}
|
|
2361
|
+
return `${result.servers.length} stale server${result.servers.length === 1 ? "" : "s"}`;
|
|
2362
|
+
});
|
|
2367
2363
|
check("hooks file", () => ensureHooksFile());
|
|
2368
2364
|
const here = findProjectForCwd();
|
|
2369
2365
|
reportDispatcher();
|
|
@@ -2810,11 +2806,13 @@ function cmdStatusline() {
|
|
|
2810
2806
|
parts.push("store ahead (update hive)");
|
|
2811
2807
|
let turns = null;
|
|
2812
2808
|
let budget = null;
|
|
2809
|
+
let handoff = null;
|
|
2813
2810
|
try {
|
|
2814
2811
|
const actorId = process.env.HIVE_AGENT_ID;
|
|
2815
2812
|
if (actorId) {
|
|
2816
|
-
const lead = db.prepare("SELECT
|
|
2813
|
+
const lead = db.prepare("SELECT id AS lead FROM agents WHERE project_id = ? AND actor_id = ? AND kind = 'lead' AND status = 'running'").get(project.id, actorId);
|
|
2817
2814
|
if (lead) {
|
|
2815
|
+
handoff = handoffStatusSegment(lead.lead);
|
|
2818
2816
|
let inputPath = "";
|
|
2819
2817
|
if (!process.stdin.isTTY) {
|
|
2820
2818
|
try {
|
|
@@ -2834,6 +2832,8 @@ function cmdStatusline() {
|
|
|
2834
2832
|
const color = budget === null ? "" : turns >= budget.stop ? "\x1b[31m" : turns >= budget.warn ? "\x1b[33m" : "";
|
|
2835
2833
|
parts.push(`${color}turns ${turns}${color ? "\x1b[0m" : ""}`);
|
|
2836
2834
|
}
|
|
2835
|
+
if (handoff !== null)
|
|
2836
|
+
parts.push(handoff);
|
|
2837
2837
|
console.log(`\x1b[33m⬡\x1b[0m \x1b[2mhive:\x1b[0m ${parts.join(" \x1b[2m·\x1b[0m ")}`);
|
|
2838
2838
|
}
|
|
2839
2839
|
function padSlug(name) {
|
|
@@ -2857,19 +2857,20 @@ function canonicalPath(path) {
|
|
|
2857
2857
|
function nextCandidates() {
|
|
2858
2858
|
const queenRoot = canonicalPath(join(storeDir(), "queen"));
|
|
2859
2859
|
const byCount = (a, b) => b - a;
|
|
2860
|
+
const waitsOnYou = (p) => p.needs_human + p.questions_pending;
|
|
2860
2861
|
return collectPortfolio()
|
|
2861
|
-
.projects.filter((p) => (p.lane === "waiting_on_you" || p.lane === "stuck" || (p.lane === "moving" && p
|
|
2862
|
+
.projects.filter((p) => (p.lane === "waiting_on_you" || p.lane === "stuck" || (p.lane === "moving" && waitsOnYou(p) > 0)) &&
|
|
2862
2863
|
canonicalPath(p.root) !== queenRoot)
|
|
2863
2864
|
.sort((a, b) => {
|
|
2864
|
-
const tier = (p) => (p.lane === "waiting_on_you" ? 0 : p
|
|
2865
|
+
const tier = (p) => (p.lane === "waiting_on_you" ? 0 : waitsOnYou(p) > 0 ? 1 : 2);
|
|
2865
2866
|
if (tier(a) !== tier(b))
|
|
2866
2867
|
return tier(a) - tier(b);
|
|
2867
2868
|
if (a.lane === "waiting_on_you") {
|
|
2868
|
-
return (byCount(a
|
|
2869
|
+
return (byCount(waitsOnYou(a), waitsOnYou(b)) ||
|
|
2869
2870
|
a.last_activity_at.localeCompare(b.last_activity_at) ||
|
|
2870
2871
|
a.id - b.id);
|
|
2871
2872
|
}
|
|
2872
|
-
return (byCount(a
|
|
2873
|
+
return (byCount(waitsOnYou(a), waitsOnYou(b)) ||
|
|
2873
2874
|
byCount(a.todos.blocked_in_progress, b.todos.blocked_in_progress) ||
|
|
2874
2875
|
byCount(a.wakes.overdue, b.wakes.overdue) ||
|
|
2875
2876
|
byCount(a.workers.needs_input, b.workers.needs_input) ||
|
|
@@ -2948,7 +2949,7 @@ function cmdQueenAudit(argv) {
|
|
|
2948
2949
|
return;
|
|
2949
2950
|
}
|
|
2950
2951
|
if (report.entries.length === 0) {
|
|
2951
|
-
console.log("No queen
|
|
2952
|
+
console.log("No queen audit entries recorded.");
|
|
2952
2953
|
return;
|
|
2953
2954
|
}
|
|
2954
2955
|
for (const entry of report.entries) {
|
|
@@ -3006,7 +3007,7 @@ function cmdPortfolio(argv) {
|
|
|
3006
3007
|
` lead ${leadText(p)}; workers ${w.working} working, ${w.idle} idle, ${w.needs_input} needs input, ` +
|
|
3007
3008
|
`${w.other} other, ${w.unreachable} unreachable, ${w.unconfirmed} unconfirmed\n` +
|
|
3008
3009
|
` todos ${t.open} open, ${t.in_progress} in progress, ${t.blocked} blocked, ${t.high} high; ` +
|
|
3009
|
-
`needs human ${p.needs_human}; wakes ${p.wakes.pending} pending, ${p.wakes.overdue} overdue\n` +
|
|
3010
|
+
`needs human ${p.needs_human}; questions ${p.questions_pending} pending; wakes ${p.wakes.pending} pending, ${p.wakes.overdue} overdue\n` +
|
|
3010
3011
|
` last activity ${p.last_activity_at} UTC`);
|
|
3011
3012
|
}
|
|
3012
3013
|
const l = report.totals.lanes;
|
|
@@ -3314,6 +3315,70 @@ function cmdTodo(argv) {
|
|
|
3314
3315
|
}
|
|
3315
3316
|
}
|
|
3316
3317
|
}
|
|
3318
|
+
function cmdQuestions(argv) {
|
|
3319
|
+
const parsed = parseArgs(argv, { flags: ["--json"] });
|
|
3320
|
+
if (parsed.positional.length > 0)
|
|
3321
|
+
parsed.unknown.push(parsed.positional[0]);
|
|
3322
|
+
rejectUnknownFlags("questions", parsed, "--json");
|
|
3323
|
+
const project = resolveProjectForRead("questions");
|
|
3324
|
+
const questions = listQuestions(project.id);
|
|
3325
|
+
if (parsed.flags.has("--json")) {
|
|
3326
|
+
console.log(JSON.stringify({ questions }));
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
if (questions.length === 0) {
|
|
3330
|
+
console.log("No pending or defaulted questions.");
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
for (const q of questions) {
|
|
3334
|
+
console.log(`#${q.id} [${q.state}] ${q.header}`);
|
|
3335
|
+
console.log(`Question: ${q.question}`);
|
|
3336
|
+
console.log(`Default: ${q.default_answer}`);
|
|
3337
|
+
if (q.options) {
|
|
3338
|
+
console.log("Options:");
|
|
3339
|
+
for (const option of q.options) {
|
|
3340
|
+
console.log(` ${option.label}${option.recommended ? " (Recommended)" : ""}: ${option.description}`);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
function cmdAnswer(argv) {
|
|
3346
|
+
const endOfFlags = argv.indexOf("--");
|
|
3347
|
+
const optionArgs = endOfFlags === -1 ? argv : argv.slice(0, endOfFlags);
|
|
3348
|
+
if (optionArgs.filter((arg) => arg === "--notes").length > 1) {
|
|
3349
|
+
console.error("hive answer: --notes may be passed only once.");
|
|
3350
|
+
process.exit(1);
|
|
3351
|
+
}
|
|
3352
|
+
const parsed = parseArgs(argv, { valued: ["--notes"] });
|
|
3353
|
+
rejectUnknownFlags("answer", parsed, "--notes <text>");
|
|
3354
|
+
requireFlagValues("answer", parsed);
|
|
3355
|
+
if (parsed.values.has("--notes") && parsed.values.get("--notes") === undefined) {
|
|
3356
|
+
console.error("hive answer: --notes requires a value.");
|
|
3357
|
+
process.exit(1);
|
|
3358
|
+
}
|
|
3359
|
+
if (parsed.positional.length !== 2) {
|
|
3360
|
+
console.error("hive answer: usage: hive answer <positive id> <one answer argument> [--notes <text>]");
|
|
3361
|
+
process.exit(1);
|
|
3362
|
+
}
|
|
3363
|
+
const [rawId, answer] = parsed.positional;
|
|
3364
|
+
if (!/^\d+$/.test(rawId) || !Number.isSafeInteger(Number(rawId)) || Number(rawId) <= 0) {
|
|
3365
|
+
console.error(`hive answer: question id must be a positive safe integer, got ${JSON.stringify(rawId)}.`);
|
|
3366
|
+
process.exit(1);
|
|
3367
|
+
}
|
|
3368
|
+
const project = resolveProjectForRead("answer");
|
|
3369
|
+
const id = Number(rawId);
|
|
3370
|
+
const receipt = answerQuestion(project.id, currentActor(), id, answer, parsed.values.get("--notes"));
|
|
3371
|
+
console.log(`Question #${receipt.question_id} answered.`);
|
|
3372
|
+
if (receipt.wake_id === null) {
|
|
3373
|
+
console.log(`No wake queued: ${receipt.wake_skipped ?? "the asking session could not be reached"}.`);
|
|
3374
|
+
}
|
|
3375
|
+
else if (receipt.wake_rerouted_to) {
|
|
3376
|
+
console.log(`Wake #${receipt.wake_id} queued for ${receipt.wake_rerouted_to}; the asker is not running.`);
|
|
3377
|
+
}
|
|
3378
|
+
else {
|
|
3379
|
+
console.log(`Wake #${receipt.wake_id} queued for the asking session.`);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3317
3382
|
function cmdPads() {
|
|
3318
3383
|
const project = resolveProjectForRead("pads");
|
|
3319
3384
|
const pads = listActivePads(project.id);
|
|
@@ -3435,7 +3500,10 @@ if (command === "--version" || command === "-v") {
|
|
|
3435
3500
|
const COMMANDS = [
|
|
3436
3501
|
"lead", "queen", "queen-audit", "init", "attach", "start", "stop", "show", "hide", "status", "portfolio", "crew", "next", "setup", "upgrade", "doctor",
|
|
3437
3502
|
LEAD_PANE_EXITED_VERB,
|
|
3438
|
-
|
|
3503
|
+
LEAD_HANDOFF_GRACE_VERB,
|
|
3504
|
+
LEAD_HANDOFF_START_VERB,
|
|
3505
|
+
"lead-handoff",
|
|
3506
|
+
"pads", "pad", "todos", "todo", "questions", "answer", "backups", "restore", "project", "runbook", "posture", "profile", "kickoff", "statusline",
|
|
3439
3507
|
];
|
|
3440
3508
|
if (!COMMANDS.includes(command)) {
|
|
3441
3509
|
if (command.startsWith("--")) {
|
|
@@ -3475,6 +3543,15 @@ try {
|
|
|
3475
3543
|
case LEAD_PANE_EXITED_VERB:
|
|
3476
3544
|
cmdLeadPaneExited(rest);
|
|
3477
3545
|
break;
|
|
3546
|
+
case "lead-handoff":
|
|
3547
|
+
process.exitCode = await cmdLeadHandoff(rest);
|
|
3548
|
+
break;
|
|
3549
|
+
case LEAD_HANDOFF_GRACE_VERB:
|
|
3550
|
+
process.exitCode = await runHandoffGrace(rest);
|
|
3551
|
+
break;
|
|
3552
|
+
case LEAD_HANDOFF_START_VERB:
|
|
3553
|
+
process.exitCode = await runHandoffStart(rest);
|
|
3554
|
+
break;
|
|
3478
3555
|
case "show":
|
|
3479
3556
|
cmdShow(rest);
|
|
3480
3557
|
break;
|
|
@@ -3517,6 +3594,12 @@ try {
|
|
|
3517
3594
|
case "todo":
|
|
3518
3595
|
cmdTodo(rest);
|
|
3519
3596
|
break;
|
|
3597
|
+
case "questions":
|
|
3598
|
+
cmdQuestions(rest);
|
|
3599
|
+
break;
|
|
3600
|
+
case "answer":
|
|
3601
|
+
cmdAnswer(rest);
|
|
3602
|
+
break;
|
|
3520
3603
|
case "backups":
|
|
3521
3604
|
cmdBackups();
|
|
3522
3605
|
break;
|
package/dist/context.js
CHANGED
|
@@ -5,6 +5,8 @@ import { basename, dirname, join, resolve, sep } from "node:path";
|
|
|
5
5
|
import { storeDir } from "./dataDir.js";
|
|
6
6
|
import { db } from "./db.js";
|
|
7
7
|
import { LEAD_KIND } from "./spawn.js";
|
|
8
|
+
import { rowOwnership } from "./tmux.js";
|
|
9
|
+
import { recordQueenRefusal } from "./queenAudit.js";
|
|
8
10
|
let selectedId = null;
|
|
9
11
|
let cachedActorId = null;
|
|
10
12
|
let lastTouchMs = 0;
|
|
@@ -24,7 +26,7 @@ export function currentActor() {
|
|
|
24
26
|
return cachedActorId;
|
|
25
27
|
}
|
|
26
28
|
const envId = process.env.HIVE_AGENT_ID;
|
|
27
|
-
const id = envId ??
|
|
29
|
+
const id = envId ?? `${HUMAN_ACTOR_PREFIX}${userInfo().username}`;
|
|
28
30
|
const name = process.env.HIVE_AGENT_NAME ?? id;
|
|
29
31
|
const kind = envId ? "agent" : "human";
|
|
30
32
|
db.prepare(`INSERT INTO actors (id, name, kind, tmux_pane) VALUES (?, ?, ?, ?)
|
|
@@ -294,6 +296,30 @@ export function captureQueenWriteIdentity(targetProjectId) {
|
|
|
294
296
|
return null;
|
|
295
297
|
return { actor_id: process.env.HIVE_AGENT_ID, home_project_id: home };
|
|
296
298
|
}
|
|
299
|
+
export function resolveQueenLeadTarget(operation, targetProjectId, ref, resolve) {
|
|
300
|
+
if (!captureQueenWriteIdentity(targetProjectId))
|
|
301
|
+
return resolve();
|
|
302
|
+
let agent;
|
|
303
|
+
try {
|
|
304
|
+
agent = resolve();
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
throw queenRefusal(operation, targetProjectId, `recipient ${JSON.stringify(ref)} could not be resolved: ${e instanceof Error ? e.message : String(e)}`);
|
|
308
|
+
}
|
|
309
|
+
const ownership = rowOwnership(agent);
|
|
310
|
+
const problem = agent.project_id !== targetProjectId
|
|
311
|
+
? `Agent ${agent.id} ("${agent.name}") belongs to project ${agent.project_id}, not project ${targetProjectId}`
|
|
312
|
+
: agent.kind !== LEAD_KIND
|
|
313
|
+
? `Agent ${agent.id} ("${agent.name}") is kind "${agent.kind}", not a lead`
|
|
314
|
+
: agent.status !== "running"
|
|
315
|
+
? `Agent ${agent.id} ("${agent.name}") is ${agent.status}, not running`
|
|
316
|
+
: ownership !== "live"
|
|
317
|
+
? `Agent ${agent.id} ("${agent.name}") does not own a live pane (ownership: ${ownership})`
|
|
318
|
+
: null;
|
|
319
|
+
if (problem)
|
|
320
|
+
throw queenRefusal(operation, targetProjectId, problem);
|
|
321
|
+
return agent;
|
|
322
|
+
}
|
|
297
323
|
export const QUEEN_REACH = {
|
|
298
324
|
whoami: "read",
|
|
299
325
|
help: "read",
|
|
@@ -330,6 +356,11 @@ export const QUEEN_REACH = {
|
|
|
330
356
|
todo_unblock: "home",
|
|
331
357
|
kv_set: "home",
|
|
332
358
|
kv_delete: "home",
|
|
359
|
+
question_ask: "home",
|
|
360
|
+
question_get: "home",
|
|
361
|
+
question_list: "home",
|
|
362
|
+
question_update: "home",
|
|
363
|
+
question_answer: "home",
|
|
333
364
|
lease_acquire: "home",
|
|
334
365
|
lease_release: "home",
|
|
335
366
|
agent_spawn: "home",
|
|
@@ -374,7 +405,7 @@ function foreignWriteProblem(reach, targetProjectId, args) {
|
|
|
374
405
|
const lead = runningLead(targetProjectId);
|
|
375
406
|
if (!lead)
|
|
376
407
|
return "that project has no running lead to address";
|
|
377
|
-
if (
|
|
408
|
+
if (args.deliver_to == null) {
|
|
378
409
|
return `deliver_to must name that project's running lead ("${lead.name}" or agent id ${lead.id})`;
|
|
379
410
|
}
|
|
380
411
|
return null;
|
|
@@ -397,8 +428,8 @@ function foreignWriteProblem(reach, targetProjectId, args) {
|
|
|
397
428
|
const lead = runningLead(targetProjectId);
|
|
398
429
|
if (!lead)
|
|
399
430
|
return "that project has no running lead to address";
|
|
400
|
-
if (
|
|
401
|
-
return `the recipient must
|
|
431
|
+
if (args.agent_id == null && args.name == null) {
|
|
432
|
+
return `the recipient must name that project's running lead ("${lead.name}" or agent id ${lead.id})`;
|
|
402
433
|
}
|
|
403
434
|
return null;
|
|
404
435
|
}
|
|
@@ -441,7 +472,20 @@ export function assertQueenToolCall(tool, args) {
|
|
|
441
472
|
function queenRefusal(operation, targetProjectId, problem) {
|
|
442
473
|
const target = targetProjectId === null ? null : getProject(targetProjectId);
|
|
443
474
|
const where = target ? `in project ${target.id} ("${target.name}")` : "outside the queen's own project";
|
|
444
|
-
|
|
475
|
+
const refusal = new Error(`${QUEEN_REFUSAL}: the queen cannot run ${operation} ${where}: ${problem}. Into another project the ` +
|
|
445
476
|
"queen may only todo_create, todo_comment, send text or wakes to its running lead, start its lead " +
|
|
446
477
|
"with `hive lead <path>`, and run project_prune or `hive project rm` on it. Everything else there is its lead's to do.");
|
|
478
|
+
if (targetProjectId !== null) {
|
|
479
|
+
const identity = captureQueenWriteIdentity(targetProjectId);
|
|
480
|
+
if (identity) {
|
|
481
|
+
try {
|
|
482
|
+
recordQueenRefusal(operation, targetProjectId, problem, identity);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// A refusal must survive an unavailable audit write.
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return refusal;
|
|
447
490
|
}
|
|
491
|
+
export const HUMAN_ACTOR_PREFIX = "user:";
|
package/dist/crew.js
CHANGED
|
@@ -5,6 +5,7 @@ import { awaitingFirstPrompt } from "./firstPrompt.js";
|
|
|
5
5
|
import { heldReasonLabel } from "./heldLabel.js";
|
|
6
6
|
import { harnessFor, paneClassifierFor } from "./harnesses.js";
|
|
7
7
|
import { NEEDS_HUMAN_TAG } from "./portfolio.js";
|
|
8
|
+
import { pendingQuestionCount } from "./questions.js";
|
|
8
9
|
import { loadProjectYml } from "./projectYml.js";
|
|
9
10
|
import { parseTags } from "./result.js";
|
|
10
11
|
import { ACTIVE_TIMER_WHERE } from "./scheduler.js";
|
|
@@ -20,6 +21,8 @@ export function crewStoreProblem(database = db) {
|
|
|
20
21
|
const columns = database.prepare("SELECT name FROM pragma_table_info('agents')").all();
|
|
21
22
|
if (!columns.some((c) => c.name === "todo_id"))
|
|
22
23
|
return "the store predates the worker-to-todo link (agents.todo_id)";
|
|
24
|
+
if (!tables.has("questions"))
|
|
25
|
+
return "the store has no questions table";
|
|
23
26
|
return null;
|
|
24
27
|
}
|
|
25
28
|
function isoUtc(value) {
|
|
@@ -235,12 +238,26 @@ function tokenize(segment) {
|
|
|
235
238
|
return tokens;
|
|
236
239
|
}
|
|
237
240
|
const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
241
|
+
const SHELL_PREFIX_KEYWORDS = new Set(["if", "elif", "while", "until", "do", "then", "else", "!", "{"]);
|
|
242
|
+
const SHELL_HEADER_KEYWORDS = new Set(["for", "select", "case"]);
|
|
243
|
+
const SHELL_CLOSING_KEYWORDS = new Set(["done", "fi", "esac", "}"]);
|
|
238
244
|
function toSegment(segment) {
|
|
239
245
|
const tokens = tokenize(segment);
|
|
240
246
|
let start = 0;
|
|
241
247
|
while (start < tokens.length && ENV_ASSIGNMENT.test(tokens[start]))
|
|
242
248
|
start++;
|
|
243
|
-
|
|
249
|
+
let rest = tokens.slice(start);
|
|
250
|
+
while (rest.length > 0) {
|
|
251
|
+
if (SHELL_HEADER_KEYWORDS.has(rest[0]) || SHELL_CLOSING_KEYWORDS.has(rest[0])) {
|
|
252
|
+
rest = [];
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (!SHELL_PREFIX_KEYWORDS.has(rest[0]))
|
|
256
|
+
break;
|
|
257
|
+
rest = rest.slice(1);
|
|
258
|
+
while (rest.length > 0 && ENV_ASSIGNMENT.test(rest[0]))
|
|
259
|
+
rest = rest.slice(1);
|
|
260
|
+
}
|
|
244
261
|
return { tokens: rest[0] === "cd" ? [] : rest, writes: writesToFile(segment) };
|
|
245
262
|
}
|
|
246
263
|
function segmentsOf(command) {
|
|
@@ -272,7 +289,7 @@ function matchesPrefix(tokens, matcher) {
|
|
|
272
289
|
return tokens.length >= matcher.tokens.length && matcher.tokens.every((re, i) => re.test(tokens[i]));
|
|
273
290
|
}
|
|
274
291
|
const READING_EXES = new Set(["rg", "grep", "cat", "head", "tail", "sed"]);
|
|
275
|
-
const FALLBACK_SKIP = new Set(["printf", "echo", "true", ":"]);
|
|
292
|
+
const FALLBACK_SKIP = new Set(["printf", "echo", "true", ":", "[", "[[", "test"]);
|
|
276
293
|
const NODE_RUNNERS = new Set(["npm", "pnpm", "yarn"]);
|
|
277
294
|
const TEST_BINARIES = new Set(["pest", "phpunit", "vitest", "jest"]);
|
|
278
295
|
const BUILD_BINARIES = new Set(["pint", "phpstan"]);
|
|
@@ -482,6 +499,7 @@ export function collectCrew(project, now) {
|
|
|
482
499
|
read_at: now.toISOString(),
|
|
483
500
|
lanes,
|
|
484
501
|
needs_you: needsYou,
|
|
502
|
+
questions: { pending: pendingQuestionCount(project.id) },
|
|
485
503
|
wakes: {
|
|
486
504
|
pending: pending.length,
|
|
487
505
|
next: pending[0]
|
package/dist/dashboard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { db } from "./db.js";
|
|
3
4
|
import { getProject } from "./context.js";
|
|
4
5
|
export const describeVisibility = (visibility) => visibility === null ? "running" : visibility === "window" ? "own window" : visibility;
|
|
@@ -12,6 +13,7 @@ export function processCounts(procs) {
|
|
|
12
13
|
}
|
|
13
14
|
import { awaitingFirstPrompt } from "./firstPrompt.js";
|
|
14
15
|
import { SLUG_MAX_LEN, cutToUnitBudget } from "./slug.js";
|
|
16
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
15
17
|
export const TODO_CAP = 100;
|
|
16
18
|
export const AGENT_CAP = 50;
|
|
17
19
|
export const WAKE_CAP = 50;
|
|
@@ -449,6 +451,45 @@ function renderWakesSection(index, projectId) {
|
|
|
449
451
|
const body = renderCappedList(shown.length, all.length, "pending wakes", "wakes", rows, emptyState("No pending wakes.", "Scheduled and idle-triggered check-ins from wake_set show up here."));
|
|
450
452
|
return section(index, "wakes", "Wakes", `${all.length} pending`, "bell", String(shown.length), body);
|
|
451
453
|
}
|
|
454
|
+
function questionAge(createdAt) {
|
|
455
|
+
const timestamp = Date.parse(`${createdAt.replace(" ", "T")}Z`);
|
|
456
|
+
if (!Number.isFinite(timestamp))
|
|
457
|
+
return "unknown";
|
|
458
|
+
const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
|
|
459
|
+
if (seconds >= 86400)
|
|
460
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
461
|
+
if (seconds >= 3600)
|
|
462
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
463
|
+
if (seconds >= 60)
|
|
464
|
+
return `${Math.floor(seconds / 60)}m ago`;
|
|
465
|
+
return "now";
|
|
466
|
+
}
|
|
467
|
+
function renderQuestionsSection(index, projectId) {
|
|
468
|
+
// A static import of questions.js cycles through tools/wakes.ts and scheduler.ts.
|
|
469
|
+
const { listQuestions, pendingQuestionCount } = requireFromHere("./questions.js");
|
|
470
|
+
const questions = listQuestions(projectId);
|
|
471
|
+
if (questions.length === 0)
|
|
472
|
+
return "";
|
|
473
|
+
const pendingCount = pendingQuestionCount(projectId);
|
|
474
|
+
const rows = questions.map((question) => {
|
|
475
|
+
const options = question.kind === "free_text"
|
|
476
|
+
? '<p class="question-options">free text</p>'
|
|
477
|
+
: `<ul class="question-options">${(question.options ?? []).map((option) => `<li>${prose(option.label)}${option.recommended ? ' <span class="recommended">recommended</span>' : ""}` +
|
|
478
|
+
`<span class="question-option-description">${prose(option.description)}</span></li>`).join("")}</ul>`;
|
|
479
|
+
const ref = question.ref ? `<span class="question-ref">${question.ref.kind === "todo" ? "todo" : "PR"} #${question.ref.number}</span>` : "";
|
|
480
|
+
const state = question.state === "defaulted"
|
|
481
|
+
? '<span class="question-state">defaulted - proceeding on the default</span>'
|
|
482
|
+
: '<span class="question-state">pending</span>';
|
|
483
|
+
return (`<li class="question" data-question="${question.id}"><div class="question-head">${mono(`#${question.id}`)} ` +
|
|
484
|
+
`<strong>${escapeHtml(question.header)}</strong> ${state}</div>` +
|
|
485
|
+
`<p class="question-text">${prose(question.question)}</p>${options}` +
|
|
486
|
+
`<p class="question-default">Default answer: ${prose(question.default_answer)}</p>` +
|
|
487
|
+
`<p class="question-meta">Asked by ${mono(question.asker)} · ${escapeHtml(questionAge(question.created_at))}` +
|
|
488
|
+
`${ref ? ` · ${ref}` : ""}</p>` +
|
|
489
|
+
`<p class="question-answer">To answer: <code>hive answer ${question.id}</code></p></li>`);
|
|
490
|
+
}).join("\n");
|
|
491
|
+
return section(index, "questions", "Questions", `${pendingCount} pending`, "inbox", String(pendingCount), `<ul class="question-list">${rows}</ul>`);
|
|
492
|
+
}
|
|
452
493
|
function fetchCommentActivity(projectId) {
|
|
453
494
|
const rows = db
|
|
454
495
|
.prepare(`SELECT c.id, c.body, c.author, c.created_at, t.id AS todo_id, t.title AS todo_title,
|
|
@@ -682,6 +723,15 @@ export const SECTION_STYLE = `
|
|
|
682
723
|
.priority-low { color: var(--fg-subtle); }
|
|
683
724
|
.blockers { font-size: 0.8125rem; color: var(--fg-muted); margin: 0 0 0.45rem 0.6rem; }
|
|
684
725
|
.wake-body { color: var(--fg); font-size: 0.875rem; overflow-wrap: anywhere; }
|
|
726
|
+
.question-list { display: grid; gap: 0.85rem; list-style: none; margin: 0; padding: 0; }
|
|
727
|
+
.question { border-bottom: 1px solid var(--border); padding-bottom: 0.85rem; overflow-wrap: anywhere; }
|
|
728
|
+
.question:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
729
|
+
.question-head, .question-meta, .question-answer, .question-default, .question-text, .question-options { margin: 0.2rem 0; }
|
|
730
|
+
.question-head, .question-meta, .question-answer { font-size: 0.8125rem; }
|
|
731
|
+
.question-state, .question-ref, .recommended { color: var(--fg-muted); font-size: 0.75rem; }
|
|
732
|
+
.question-options { padding-left: 1.2rem; }
|
|
733
|
+
.question-option-description { display: block; padding-left: 0.5rem; color: var(--fg-muted); }
|
|
734
|
+
.question-text { overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
685
735
|
.activity-body { font-size: 0.8125rem; color: var(--fg-muted); white-space: pre-wrap;
|
|
686
736
|
overflow-wrap: anywhere; margin-top: 0.1rem; }
|
|
687
737
|
|
|
@@ -1171,7 +1221,9 @@ function buildDashboard(projectId, procs) {
|
|
|
1171
1221
|
const project = fetchProject(projectId);
|
|
1172
1222
|
const stats = fetchDayStats(projectId);
|
|
1173
1223
|
const index = [];
|
|
1224
|
+
const questionsSection = renderQuestionsSection(index, projectId);
|
|
1174
1225
|
const cards = [
|
|
1226
|
+
...(questionsSection ? [questionsSection] : []),
|
|
1175
1227
|
renderThroughputSection(index, stats),
|
|
1176
1228
|
renderBoardSection(index, projectId),
|
|
1177
1229
|
renderTodosSection(index, projectId),
|