@cmgmyr/hive 1.10.1 → 1.11.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/claude-plugin/crew/model.mjs +2 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +125 -46
- package/dist/context.js +7 -1
- package/dist/crew.js +20 -2
- package/dist/db.js +139 -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 +2 -0
- package/dist/projectRemove.js +1 -0
- package/dist/projectYml.js +8 -1
- package/dist/questions.js +254 -0
- package/dist/scheduler.js +25 -5
- package/dist/staleServers.js +122 -0
- package/dist/tools/meta.js +4 -1
- package/dist/tools/questions.js +87 -0
- package/dist/tools/wakes.js +9 -9
- package/package.json +1 -1
|
@@ -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.0","sha":"7534ff6","dirty":false,"build_id":"bd31535e0eaed043ebe60b90c2e95925e21567baf82b5142e3a6c0e6908a58ac"}
|
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
|
|
|
@@ -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) {
|
|
@@ -3006,7 +3006,7 @@ function cmdPortfolio(argv) {
|
|
|
3006
3006
|
` lead ${leadText(p)}; workers ${w.working} working, ${w.idle} idle, ${w.needs_input} needs input, ` +
|
|
3007
3007
|
`${w.other} other, ${w.unreachable} unreachable, ${w.unconfirmed} unconfirmed\n` +
|
|
3008
3008
|
` 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` +
|
|
3009
|
+
`needs human ${p.needs_human}; questions ${p.questions_pending} pending; wakes ${p.wakes.pending} pending, ${p.wakes.overdue} overdue\n` +
|
|
3010
3010
|
` last activity ${p.last_activity_at} UTC`);
|
|
3011
3011
|
}
|
|
3012
3012
|
const l = report.totals.lanes;
|
|
@@ -3314,6 +3314,67 @@ function cmdTodo(argv) {
|
|
|
3314
3314
|
}
|
|
3315
3315
|
}
|
|
3316
3316
|
}
|
|
3317
|
+
function cmdQuestions(argv) {
|
|
3318
|
+
const parsed = parseArgs(argv, { flags: ["--json"] });
|
|
3319
|
+
if (parsed.positional.length > 0)
|
|
3320
|
+
parsed.unknown.push(parsed.positional[0]);
|
|
3321
|
+
rejectUnknownFlags("questions", parsed, "--json");
|
|
3322
|
+
const project = resolveProjectForRead("questions");
|
|
3323
|
+
const questions = listQuestions(project.id);
|
|
3324
|
+
if (parsed.flags.has("--json")) {
|
|
3325
|
+
console.log(JSON.stringify({ questions }));
|
|
3326
|
+
return;
|
|
3327
|
+
}
|
|
3328
|
+
if (questions.length === 0) {
|
|
3329
|
+
console.log("No pending or defaulted questions.");
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
for (const q of questions) {
|
|
3333
|
+
console.log(`#${q.id} [${q.state}] ${q.header}`);
|
|
3334
|
+
console.log(`Question: ${q.question}`);
|
|
3335
|
+
console.log(`Default: ${q.default_answer}`);
|
|
3336
|
+
if (q.options) {
|
|
3337
|
+
console.log("Options:");
|
|
3338
|
+
for (const option of q.options) {
|
|
3339
|
+
console.log(` ${option.label}${option.recommended ? " (Recommended)" : ""}: ${option.description}`);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
function cmdAnswer(argv) {
|
|
3345
|
+
const endOfFlags = argv.indexOf("--");
|
|
3346
|
+
const optionArgs = endOfFlags === -1 ? argv : argv.slice(0, endOfFlags);
|
|
3347
|
+
if (optionArgs.filter((arg) => arg === "--notes").length > 1) {
|
|
3348
|
+
console.error("hive answer: --notes may be passed only once.");
|
|
3349
|
+
process.exit(1);
|
|
3350
|
+
}
|
|
3351
|
+
const parsed = parseArgs(argv, { valued: ["--notes"] });
|
|
3352
|
+
rejectUnknownFlags("answer", parsed, "--notes <text>");
|
|
3353
|
+
requireFlagValues("answer", parsed);
|
|
3354
|
+
if (parsed.values.has("--notes") && parsed.values.get("--notes") === undefined) {
|
|
3355
|
+
console.error("hive answer: --notes requires a value.");
|
|
3356
|
+
process.exit(1);
|
|
3357
|
+
}
|
|
3358
|
+
if (parsed.positional.length !== 2) {
|
|
3359
|
+
console.error("hive answer: usage: hive answer <positive id> <one answer argument> [--notes <text>]");
|
|
3360
|
+
process.exit(1);
|
|
3361
|
+
}
|
|
3362
|
+
const [rawId, answer] = parsed.positional;
|
|
3363
|
+
if (!/^\d+$/.test(rawId) || !Number.isSafeInteger(Number(rawId)) || Number(rawId) <= 0) {
|
|
3364
|
+
console.error(`hive answer: question id must be a positive safe integer, got ${JSON.stringify(rawId)}.`);
|
|
3365
|
+
process.exit(1);
|
|
3366
|
+
}
|
|
3367
|
+
const project = resolveProjectForRead("answer");
|
|
3368
|
+
const id = Number(rawId);
|
|
3369
|
+
const receipt = answerQuestion(project.id, currentActor(), id, answer, parsed.values.get("--notes"));
|
|
3370
|
+
console.log(`Question #${receipt.question_id} answered.`);
|
|
3371
|
+
if (receipt.wake_id === null) {
|
|
3372
|
+
console.log(`No wake queued: ${receipt.wake_skipped ?? "the asking session could not be reached"}.`);
|
|
3373
|
+
}
|
|
3374
|
+
else {
|
|
3375
|
+
console.log(`Wake #${receipt.wake_id} queued for the asking session.`);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3317
3378
|
function cmdPads() {
|
|
3318
3379
|
const project = resolveProjectForRead("pads");
|
|
3319
3380
|
const pads = listActivePads(project.id);
|
|
@@ -3435,7 +3496,10 @@ if (command === "--version" || command === "-v") {
|
|
|
3435
3496
|
const COMMANDS = [
|
|
3436
3497
|
"lead", "queen", "queen-audit", "init", "attach", "start", "stop", "show", "hide", "status", "portfolio", "crew", "next", "setup", "upgrade", "doctor",
|
|
3437
3498
|
LEAD_PANE_EXITED_VERB,
|
|
3438
|
-
|
|
3499
|
+
LEAD_HANDOFF_GRACE_VERB,
|
|
3500
|
+
LEAD_HANDOFF_START_VERB,
|
|
3501
|
+
"lead-handoff",
|
|
3502
|
+
"pads", "pad", "todos", "todo", "questions", "answer", "backups", "restore", "project", "runbook", "posture", "profile", "kickoff", "statusline",
|
|
3439
3503
|
];
|
|
3440
3504
|
if (!COMMANDS.includes(command)) {
|
|
3441
3505
|
if (command.startsWith("--")) {
|
|
@@ -3475,6 +3539,15 @@ try {
|
|
|
3475
3539
|
case LEAD_PANE_EXITED_VERB:
|
|
3476
3540
|
cmdLeadPaneExited(rest);
|
|
3477
3541
|
break;
|
|
3542
|
+
case "lead-handoff":
|
|
3543
|
+
process.exitCode = await cmdLeadHandoff(rest);
|
|
3544
|
+
break;
|
|
3545
|
+
case LEAD_HANDOFF_GRACE_VERB:
|
|
3546
|
+
process.exitCode = await runHandoffGrace(rest);
|
|
3547
|
+
break;
|
|
3548
|
+
case LEAD_HANDOFF_START_VERB:
|
|
3549
|
+
process.exitCode = await runHandoffStart(rest);
|
|
3550
|
+
break;
|
|
3478
3551
|
case "show":
|
|
3479
3552
|
cmdShow(rest);
|
|
3480
3553
|
break;
|
|
@@ -3517,6 +3590,12 @@ try {
|
|
|
3517
3590
|
case "todo":
|
|
3518
3591
|
cmdTodo(rest);
|
|
3519
3592
|
break;
|
|
3593
|
+
case "questions":
|
|
3594
|
+
cmdQuestions(rest);
|
|
3595
|
+
break;
|
|
3596
|
+
case "answer":
|
|
3597
|
+
cmdAnswer(rest);
|
|
3598
|
+
break;
|
|
3520
3599
|
case "backups":
|
|
3521
3600
|
cmdBackups();
|
|
3522
3601
|
break;
|
package/dist/context.js
CHANGED
|
@@ -24,7 +24,7 @@ export function currentActor() {
|
|
|
24
24
|
return cachedActorId;
|
|
25
25
|
}
|
|
26
26
|
const envId = process.env.HIVE_AGENT_ID;
|
|
27
|
-
const id = envId ??
|
|
27
|
+
const id = envId ?? `${HUMAN_ACTOR_PREFIX}${userInfo().username}`;
|
|
28
28
|
const name = process.env.HIVE_AGENT_NAME ?? id;
|
|
29
29
|
const kind = envId ? "agent" : "human";
|
|
30
30
|
db.prepare(`INSERT INTO actors (id, name, kind, tmux_pane) VALUES (?, ?, ?, ?)
|
|
@@ -330,6 +330,11 @@ export const QUEEN_REACH = {
|
|
|
330
330
|
todo_unblock: "home",
|
|
331
331
|
kv_set: "home",
|
|
332
332
|
kv_delete: "home",
|
|
333
|
+
question_ask: "home",
|
|
334
|
+
question_get: "home",
|
|
335
|
+
question_list: "home",
|
|
336
|
+
question_update: "home",
|
|
337
|
+
question_answer: "home",
|
|
333
338
|
lease_acquire: "home",
|
|
334
339
|
lease_release: "home",
|
|
335
340
|
agent_spawn: "home",
|
|
@@ -445,3 +450,4 @@ function queenRefusal(operation, targetProjectId, problem) {
|
|
|
445
450
|
"queen may only todo_create, todo_comment, send text or wakes to its running lead, start its lead " +
|
|
446
451
|
"with `hive lead <path>`, and run project_prune or `hive project rm` on it. Everything else there is its lead's to do.");
|
|
447
452
|
}
|
|
453
|
+
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/db.js
CHANGED
|
@@ -430,6 +430,145 @@ ALTER TABLE agent_messages ADD COLUMN sender_tag TEXT;
|
|
|
430
430
|
CREATE INDEX idx_agent_messages_delivery ON agent_messages(delivery_status, id) WHERE delivery_status IS NOT NULL;
|
|
431
431
|
`,
|
|
432
432
|
`ALTER TABLE agents ADD COLUMN todo_id INTEGER REFERENCES todos(id) ON DELETE SET NULL;`,
|
|
433
|
+
`
|
|
434
|
+
ALTER TABLE lead_turn_state ADD COLUMN snapshot_pane_pid TEXT NOT NULL DEFAULT '';
|
|
435
|
+
ALTER TABLE lead_turn_state ADD COLUMN snapshot_session_id TEXT NOT NULL DEFAULT '';
|
|
436
|
+
ALTER TABLE lead_turn_state ADD COLUMN prompt_seq INTEGER NOT NULL DEFAULT 0;
|
|
437
|
+
ALTER TABLE lead_turn_state ADD COLUMN human_prompt_seq INTEGER NOT NULL DEFAULT 0;
|
|
438
|
+
ALTER TABLE lead_turn_state ADD COLUMN human_prompt_at TEXT;
|
|
439
|
+
ALTER TABLE lead_turn_state ADD COLUMN transcript_path TEXT NOT NULL DEFAULT '';
|
|
440
|
+
ALTER TABLE lead_turn_state ADD COLUMN stop_prompt_seq INTEGER;
|
|
441
|
+
ALTER TABLE lead_turn_state ADD COLUMN stop_background TEXT;
|
|
442
|
+
ALTER TABLE lead_turn_state ADD COLUMN stop_at TEXT;
|
|
443
|
+
|
|
444
|
+
CREATE TABLE lead_handoffs (
|
|
445
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
446
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
447
|
+
lead_agent_id INTEGER NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
448
|
+
pane_target TEXT NOT NULL,
|
|
449
|
+
tmux_socket TEXT NOT NULL DEFAULT '',
|
|
450
|
+
predecessor_pane_pid TEXT NOT NULL,
|
|
451
|
+
predecessor_session_id TEXT NOT NULL,
|
|
452
|
+
reason TEXT NOT NULL CHECK (reason IN ('warn', 'stop')),
|
|
453
|
+
state TEXT NOT NULL CHECK (state IN ('pending', 'wind_down', 'requested', 'grace', 'postponed',
|
|
454
|
+
'respawning', 'started', 'completed', 'failed', 'ambiguous')),
|
|
455
|
+
pass INTEGER NOT NULL DEFAULT 1,
|
|
456
|
+
request_reason TEXT CHECK (request_reason IN ('warn', 'stop')),
|
|
457
|
+
pass_started_at TEXT,
|
|
458
|
+
request_wake_id INTEGER,
|
|
459
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
460
|
+
owner_token TEXT,
|
|
461
|
+
owner_pid INTEGER,
|
|
462
|
+
grace_seconds INTEGER,
|
|
463
|
+
grace_started_at TEXT,
|
|
464
|
+
human_prompt_baseline INTEGER,
|
|
465
|
+
pad_id INTEGER,
|
|
466
|
+
pad_revision INTEGER,
|
|
467
|
+
pad_length INTEGER,
|
|
468
|
+
pad_sha256 TEXT,
|
|
469
|
+
respawn_claimed_at TEXT,
|
|
470
|
+
predecessor_turns INTEGER,
|
|
471
|
+
hold_since TEXT,
|
|
472
|
+
hold_released_at TEXT,
|
|
473
|
+
blocked_reason TEXT,
|
|
474
|
+
successor_pane_pid TEXT,
|
|
475
|
+
successor_session_id TEXT,
|
|
476
|
+
delivered_pad_id INTEGER,
|
|
477
|
+
delivered_pad_revision INTEGER,
|
|
478
|
+
started_at TEXT,
|
|
479
|
+
completed_at TEXT,
|
|
480
|
+
failure TEXT,
|
|
481
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
|
|
482
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
|
|
483
|
+
);
|
|
484
|
+
CREATE UNIQUE INDEX idx_lead_handoffs_epoch
|
|
485
|
+
ON lead_handoffs(project_id, lead_agent_id, predecessor_pane_pid, predecessor_session_id);
|
|
486
|
+
CREATE UNIQUE INDEX idx_lead_handoffs_active
|
|
487
|
+
ON lead_handoffs(lead_agent_id) WHERE state NOT IN ('completed', 'failed');
|
|
488
|
+
`,
|
|
489
|
+
`
|
|
490
|
+
CREATE TRIGGER fence_handoff_held_claim
|
|
491
|
+
BEFORE UPDATE OF fire_count, delivery_method ON wakes
|
|
492
|
+
FOR EACH ROW
|
|
493
|
+
WHEN (NEW.fire_count > OLD.fire_count
|
|
494
|
+
OR (NEW.delivery_method IS 'pty-after-socket-timeout'
|
|
495
|
+
AND OLD.delivery_method IS NOT 'pty-after-socket-timeout'
|
|
496
|
+
AND NEW.typed_at IS NULL))
|
|
497
|
+
AND EXISTS (
|
|
498
|
+
SELECT 1 FROM lead_handoffs h
|
|
499
|
+
WHERE h.lead_agent_id = (SELECT a.id FROM agents a
|
|
500
|
+
WHERE a.actor_id = NEW.deliver_actor
|
|
501
|
+
ORDER BY (a.status = 'running') DESC,
|
|
502
|
+
a.id DESC
|
|
503
|
+
LIMIT 1)
|
|
504
|
+
AND h.state NOT IN ('completed', 'failed')
|
|
505
|
+
AND h.request_wake_id IS NOT NEW.id
|
|
506
|
+
AND (h.state = 'grace'
|
|
507
|
+
OR (h.hold_released_at IS NULL AND (
|
|
508
|
+
h.state IN ('respawning', 'ambiguous', 'wind_down')
|
|
509
|
+
OR (h.state = 'requested'
|
|
510
|
+
AND (h.reason = 'stop' OR h.pass >= 2))
|
|
511
|
+
OR (h.state = 'postponed' AND h.reason = 'stop'))))
|
|
512
|
+
AND (julianday('now') - julianday(h.updated_at)) * 86400 < 3600)
|
|
513
|
+
BEGIN
|
|
514
|
+
SELECT RAISE(IGNORE);
|
|
515
|
+
END;
|
|
516
|
+
`,
|
|
517
|
+
`DROP TRIGGER fence_handoff_held_claim;`,
|
|
518
|
+
`
|
|
519
|
+
CREATE TRIGGER fence_handoff_held_claim
|
|
520
|
+
BEFORE UPDATE OF fire_count, delivery_method ON wakes
|
|
521
|
+
FOR EACH ROW
|
|
522
|
+
WHEN (NEW.fire_count > OLD.fire_count
|
|
523
|
+
OR (NEW.delivery_method IS 'pty-after-socket-timeout'
|
|
524
|
+
AND OLD.delivery_method IS NOT 'pty-after-socket-timeout'
|
|
525
|
+
AND NEW.typed_at IS NULL))
|
|
526
|
+
AND EXISTS (
|
|
527
|
+
SELECT 1 FROM lead_handoffs h
|
|
528
|
+
WHERE h.lead_agent_id = (SELECT a.id FROM agents a
|
|
529
|
+
WHERE a.actor_id = NEW.deliver_actor
|
|
530
|
+
ORDER BY (a.status = 'running') DESC,
|
|
531
|
+
a.id DESC
|
|
532
|
+
LIMIT 1)
|
|
533
|
+
AND h.state NOT IN ('completed', 'failed')
|
|
534
|
+
AND h.request_wake_id IS NOT NEW.id
|
|
535
|
+
AND (h.state IN ('grace', 'started')
|
|
536
|
+
OR (h.hold_released_at IS NULL AND (
|
|
537
|
+
h.state IN ('respawning', 'ambiguous', 'wind_down')
|
|
538
|
+
OR (h.state = 'requested'
|
|
539
|
+
AND (h.reason = 'stop' OR h.pass >= 2))
|
|
540
|
+
OR (h.state = 'postponed' AND h.reason = 'stop'))))
|
|
541
|
+
AND (julianday('now') - julianday(h.updated_at)) * 86400 < 3600)
|
|
542
|
+
BEGIN
|
|
543
|
+
SELECT RAISE(IGNORE);
|
|
544
|
+
END;
|
|
545
|
+
`,
|
|
546
|
+
`
|
|
547
|
+
CREATE TABLE questions (
|
|
548
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
549
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
550
|
+
asker TEXT NOT NULL,
|
|
551
|
+
kind TEXT NOT NULL CHECK (kind IN ('choice', 'free_text')),
|
|
552
|
+
question TEXT NOT NULL CHECK (trim(question) != ''),
|
|
553
|
+
header TEXT NOT NULL CHECK (trim(header) != ''),
|
|
554
|
+
options TEXT,
|
|
555
|
+
default_answer TEXT NOT NULL CHECK (trim(default_answer) != ''),
|
|
556
|
+
ref_kind TEXT CHECK (ref_kind IN ('todo', 'pr')),
|
|
557
|
+
ref_number INTEGER CHECK (ref_number > 0),
|
|
558
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'answered', 'defaulted', 'withdrawn')),
|
|
559
|
+
answer TEXT,
|
|
560
|
+
notes TEXT,
|
|
561
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
562
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
563
|
+
updated_by TEXT NOT NULL,
|
|
564
|
+
answered_by TEXT,
|
|
565
|
+
CHECK ((ref_kind IS NULL) = (ref_number IS NULL)),
|
|
566
|
+
CHECK ((kind = 'choice') = (options IS NOT NULL))
|
|
567
|
+
);
|
|
568
|
+
CREATE UNIQUE INDEX idx_questions_pending_ref ON questions(project_id, ref_kind, ref_number)
|
|
569
|
+
WHERE state = 'pending' AND ref_kind IS NOT NULL;
|
|
570
|
+
CREATE INDEX idx_questions_project_state ON questions(project_id, state, id);
|
|
571
|
+
`,
|
|
433
572
|
];
|
|
434
573
|
export function storeSchemaAhead(database) {
|
|
435
574
|
try {
|
package/dist/help.js
CHANGED
|
@@ -40,6 +40,9 @@ KV — small shared status values (with optional TTL)
|
|
|
40
40
|
LEASES — advisory expiring claims on shared state
|
|
41
41
|
lease_acquire, lease_release
|
|
42
42
|
|
|
43
|
+
QUESTIONS — open questions for the human, recorded in the store
|
|
44
|
+
question_ask, question_get, question_list, question_update, question_answer
|
|
45
|
+
|
|
43
46
|
PROJECTS — scope management
|
|
44
47
|
project_list, project_add, project_select, project_prune
|
|
45
48
|
|
|
@@ -246,6 +249,9 @@ Delivery contract:
|
|
|
246
249
|
and the next action. Plain English, no markup.
|
|
247
250
|
Waking your OWN lead pane is the exception: it is not cold, so carry the
|
|
248
251
|
action and the ids and point at where the detail lives.
|
|
252
|
+
With lead_turn_budget.auto_handoff on, a lead past its warn budget gets
|
|
253
|
+
a "hive lead handoff request #N" wake: follow its steps, then run
|
|
254
|
+
hive lead-handoff. A human prompt during the grace postpones it.
|
|
249
255
|
|
|
250
256
|
Receiving wake-ups:
|
|
251
257
|
Spawned workers can always receive (deliver_to their name or id).
|
|
@@ -378,6 +384,27 @@ todo_list without marking anything done.`,
|
|
|
378
384
|
|
|
379
385
|
Use kv for values another session needs to discover: current branch, a port,
|
|
380
386
|
a phase flag. Use pads for anything longer than a line or two.`,
|
|
387
|
+
questions: `QUESTIONS — open questions for the human, recorded in the store
|
|
388
|
+
|
|
389
|
+
question_ask(kind, question, header, default_answer, options?, ref?) —
|
|
390
|
+
record a question. kind=choice takes 2-4 options (label, description,
|
|
391
|
+
recommended; exactly one recommended, and default_answer is its label).
|
|
392
|
+
kind=free_text takes no options. header is 12 characters or fewer. ref
|
|
393
|
+
({kind: "todo"|"pr", number}) is the dedupe key: asking again with a ref
|
|
394
|
+
that has a pending question in this project updates that row. The
|
|
395
|
+
receipt lists similar pending questions so you can question_update one.
|
|
396
|
+
question_get(question_id) — one question, any state, with its answer
|
|
397
|
+
question_list() — pending questions, then defaulted ones, oldest first
|
|
398
|
+
question_update(question_id, ...) — edit a pending question's text, or set
|
|
399
|
+
state to defaulted (you acted on your default) or withdrawn (moot)
|
|
400
|
+
question_answer(question_id, answer, notes?) — record the human's answer
|
|
401
|
+
to a pending or defaulted question
|
|
402
|
+
|
|
403
|
+
hive stores the question; it never puts it to the human. Render it in your
|
|
404
|
+
own turn. Answering queues a wake to the asker carrying the question,
|
|
405
|
+
answer and notes, unless the answerer is the asker or the asker cannot be
|
|
406
|
+
reached; the answer is kept either way. Nothing expires or defaults on its
|
|
407
|
+
own: only the asker or the human moves a question out of pending.`,
|
|
381
408
|
leases: `LEASES — advisory expiring claims on shared state
|
|
382
409
|
|
|
383
410
|
lease_acquire(key, ttl_seconds) — non-blocking; returns the holder if
|
package/dist/hook.js
CHANGED
|
@@ -177,6 +177,13 @@ async function stopProcessesForEndedLead(actorId, payload) {
|
|
|
177
177
|
.get(actorId);
|
|
178
178
|
if (!row)
|
|
179
179
|
return;
|
|
180
|
+
// A deliberate handoff's predecessor ending is not the lead ending; its successor keeps the crew.
|
|
181
|
+
const lead = db.prepare("SELECT id FROM agents WHERE actor_id = ? AND kind = 'lead' AND status = 'running'").get(actorId);
|
|
182
|
+
if (lead !== undefined) {
|
|
183
|
+
const { predecessorFence } = await import("./leadState.js");
|
|
184
|
+
if (predecessorFence(lead.id, payload.session_id))
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
180
187
|
// Imported here and nowhere above: harnesses.ts itself pulls tmux at module scope, and every
|
|
181
188
|
// other event in this file runs on every turn of every session without needing any of it.
|
|
182
189
|
const { harnessFor } = await import("./harnesses.js");
|
|
@@ -313,7 +320,8 @@ try {
|
|
|
313
320
|
if (!guarded)
|
|
314
321
|
state = applyWorkerTransition(actorId, event);
|
|
315
322
|
// A lead's turn state is its own table; agents.agent_state stays worker-only.
|
|
316
|
-
if (process.env.HIVE_LEAD === "1" &&
|
|
323
|
+
if (process.env.HIVE_LEAD === "1" &&
|
|
324
|
+
["prompt", "stop", "session_end"].includes(event)) {
|
|
317
325
|
try {
|
|
318
326
|
const { applyLeadHook } = await import("./leadState.js");
|
|
319
327
|
applyLeadHook(actorId, event, readPayload(), () => waitingOnSubagents(actorId, readPayload()));
|
|
@@ -321,6 +329,18 @@ try {
|
|
|
321
329
|
catch {
|
|
322
330
|
}
|
|
323
331
|
}
|
|
332
|
+
if (event === "stop" && process.env.HIVE_LEAD === "1") {
|
|
333
|
+
try {
|
|
334
|
+
const started = db
|
|
335
|
+
.prepare(`SELECT 1 AS hit FROM lead_handoffs h JOIN agents a ON a.id = h.lead_agent_id
|
|
336
|
+
WHERE a.actor_id = ? AND a.kind = 'lead' AND h.state = 'started'`)
|
|
337
|
+
.get(actorId);
|
|
338
|
+
if (started !== undefined)
|
|
339
|
+
(await import("./leadHandoff.js")).completeHandoffOnStop(actorId);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
324
344
|
if (event === "session_start" && process.env.HIVE_LEAD === "1") {
|
|
325
345
|
try {
|
|
326
346
|
await registerLeadMessagingSocket(actorId);
|
|
@@ -340,7 +360,17 @@ try {
|
|
|
340
360
|
reconcileTranscriptPath(actorId, hookPayload);
|
|
341
361
|
db.prepare("UPDATE actors SET last_seen_at = datetime('now') WHERE id = ?").run(actorId);
|
|
342
362
|
}
|
|
343
|
-
|
|
363
|
+
let logState = state ?? UNCHANGED;
|
|
364
|
+
if (event === "prompt" && process.env.HIVE_LEAD === "1") {
|
|
365
|
+
try {
|
|
366
|
+
const { isHiveFirstMessage, FIRST_MESSAGE_LOG_STATE } = await import("./leadState.js");
|
|
367
|
+
if (isHiveFirstMessage(readPayload()))
|
|
368
|
+
logState = FIRST_MESSAGE_LOG_STATE;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
record(actorId, event, logState);
|
|
344
374
|
}
|
|
345
375
|
}
|
|
346
376
|
catch {
|