@cmgmyr/hive 1.11.6 → 1.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +1 -1
- package/dist/claudeWake.js +52 -4
- package/dist/cli.js +66 -41
- package/dist/crew.js +4 -3
- package/dist/dashboard.js +6 -5
- package/dist/etime.js +37 -0
- package/dist/firstPrompt.js +6 -0
- package/dist/heldLabel.js +1 -1
- package/dist/leadHandoff.js +12 -11
- package/dist/leadHandoffCli.js +22 -1
- package/dist/leadHandoffState.js +1 -2
- package/dist/leadMessage.js +13 -0
- package/dist/leadMessageDelivery.js +14 -74
- package/dist/leadState.js +3 -2
- package/dist/leaseStore.js +54 -0
- package/dist/portfolio.js +5 -2
- package/dist/ptys.js +2 -9
- package/dist/queenDashboard.js +2 -1
- package/dist/scheduler.js +40 -166
- package/dist/staleServers.js +4 -28
- package/dist/stateProvenance.js +1 -3
- package/dist/storeClock.js +4 -0
- package/dist/teardown.js +2 -1
- package/dist/tools/leases.js +5 -25
- package/dist/tools/wakes.js +7 -15
- package/dist/wakeSql.js +135 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.11.
|
|
1
|
+
{"version":"1.11.7","sha":"4861c81","dirty":false,"build_id":"2b159fa2c2519fbd7edef9cf4396ac349c3fb25ec433f76a9b91c5ad98188f43"}
|
package/dist/claudeWake.js
CHANGED
|
@@ -1,14 +1,62 @@
|
|
|
1
1
|
import { createConnection } from "node:net";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { db } from "./db.js";
|
|
4
|
+
import { loadProjectYml } from "./projectYml.js";
|
|
5
|
+
export { DELIVERY_METHOD } from "./wakeSql.js";
|
|
2
6
|
export const CROSS_SESSION_CLOSE = "</cross-session-message>";
|
|
7
|
+
export const ENVELOPE_OPEN = '<cross-session-message from="';
|
|
8
|
+
export const ENVELOPE_SENDER_CLOSE = '" from-name="hive">';
|
|
3
9
|
export const SOCKET_WAKE_FOOTER = "Automated hive wake. Do not reply to this sender.";
|
|
4
|
-
const
|
|
10
|
+
export const SOCKET_MESSAGE_FOOTER = "Automated hive worker message. Do not reply to this socket sender.";
|
|
11
|
+
export const SOCKET_WRITE_BOUND_MS = 1000;
|
|
12
|
+
export const SOCKET_CONFIRM_GRACE_SECONDS = 60;
|
|
5
13
|
export function senderAddress(socketPath) {
|
|
6
14
|
const encoded = socketPath.replace(/[^A-Za-z0-9/._-]/gu, (c) => [...Buffer.from(c)].map((b) => `%${b.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
|
|
7
15
|
return `uds:${encoded}`;
|
|
8
16
|
}
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
export function socketSenderAddress(dir) {
|
|
18
|
+
return senderAddress(join(dir, "wake-sender.sock"));
|
|
19
|
+
}
|
|
20
|
+
function claudeWakeFrame(sender, text) {
|
|
21
|
+
const content = `${ENVELOPE_OPEN}${sender}${ENVELOPE_SENDER_CLOSE}\n${text}\n${CROSS_SESSION_CLOSE}`;
|
|
22
|
+
return JSON.stringify({ type: "user", from: sender, message: { role: "user", content } }) + "\n";
|
|
23
|
+
}
|
|
24
|
+
export function quietMessagingOn(projectId) {
|
|
25
|
+
const project = db.prepare("SELECT path FROM projects WHERE id = ?").get(projectId);
|
|
26
|
+
return project !== undefined && loadProjectYml(project.path).config?.quiet_messaging === true;
|
|
27
|
+
}
|
|
28
|
+
export function promptMarkerEvidenceSql(markerAt) {
|
|
29
|
+
const evPrompt = "(CASE WHEN json_valid(ev.payload) THEN COALESCE(json_extract(ev.payload, '$.prompt'), '') ELSE ev.payload END)";
|
|
30
|
+
const headerEnd = `instr(${evPrompt}, char(10))`;
|
|
31
|
+
return `(${markerAt(evPrompt, "1")}\n OR (${headerEnd} > ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length}
|
|
32
|
+
AND substr(${evPrompt}, 1, ${ENVELOPE_OPEN.length}) = '${ENVELOPE_OPEN}'
|
|
33
|
+
AND substr(${evPrompt}, ${headerEnd} - ${ENVELOPE_SENDER_CLOSE.length}, ${ENVELOPE_SENDER_CLOSE.length}) = '${ENVELOPE_SENDER_CLOSE}'
|
|
34
|
+
AND instr(substr(${evPrompt}, ${ENVELOPE_OPEN.length + 1}, ${headerEnd} - ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length + 1}), '"') = 0
|
|
35
|
+
AND ${markerAt(evPrompt, `${headerEnd} + 1`)}
|
|
36
|
+
AND instr(${evPrompt}, char(10) || '${CROSS_SESSION_CLOSE}') > ${headerEnd}))`;
|
|
37
|
+
}
|
|
38
|
+
export function hiveWorkerEnvelopeLikeSql(promptSql) {
|
|
39
|
+
return `(${promptSql} LIKE '${ENVELOPE_OPEN}%${ENVELOPE_SENDER_CLOSE}' || char(10) || '[hive:worker %] [message #%,%' || char(10) || '${CROSS_SESSION_CLOSE}%')`;
|
|
40
|
+
}
|
|
41
|
+
export function isHiveWorkerEnvelope(prompt) {
|
|
42
|
+
const p = prompt.toLowerCase();
|
|
43
|
+
const open = ENVELOPE_OPEN.toLowerCase().replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
44
|
+
const senderClose = ENVELOPE_SENDER_CLOSE.toLowerCase().replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
45
|
+
const close = CROSS_SESSION_CLOSE.toLowerCase().replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
46
|
+
return new RegExp(`^${open}[\\s\\S]*${senderClose}\\n\\[hive:worker [\\s\\S]*\\] \\[message #[\\s\\S]*,[\\s\\S]*\\n${close}`).test(p);
|
|
47
|
+
}
|
|
48
|
+
export function socketGraceDue(actorId, attemptAt, panePid, readTurn) {
|
|
49
|
+
const firstStop = db
|
|
50
|
+
.prepare("SELECT MIN(created_at) AS t FROM agent_state_log WHERE actor_id = ? AND event = 'stop' AND created_at >= ?")
|
|
51
|
+
.get(actorId, attemptAt).t;
|
|
52
|
+
let from = firstStop;
|
|
53
|
+
if (from === null) {
|
|
54
|
+
const turn = readTurn();
|
|
55
|
+
if (turn !== null && turn.state === "working" && turn.pane_pid === panePid)
|
|
56
|
+
return false;
|
|
57
|
+
from = attemptAt;
|
|
58
|
+
}
|
|
59
|
+
return db.prepare("SELECT (julianday('now') - julianday(?)) * 86400 >= ? AS due").get(from, SOCKET_CONFIRM_GRACE_SECONDS).due === 1;
|
|
12
60
|
}
|
|
13
61
|
// True only for a completed local write. The socket sends no ack, so true never means the lead read it.
|
|
14
62
|
export function postClaudeWake(options) {
|
package/dist/cli.js
CHANGED
|
@@ -23,8 +23,8 @@ import { confirmQueenWrite, listQueenAudit } from "./queenAudit.js";
|
|
|
23
23
|
import { formatRowCounts, projectRowCounts, removeProject } from "./projectRemove.js";
|
|
24
24
|
import { ensureHooksFile, ensureLeadHooksFile } from "./hooks.js";
|
|
25
25
|
import { errorMessage, registrationNoticeText, withTrailingNewline } from "./result.js";
|
|
26
|
-
import {
|
|
27
|
-
import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
|
|
26
|
+
import { dashboardFileContained, describeStall, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, } from "./scheduler.js";
|
|
27
|
+
import { ACTIVE_TIMER_WHERE, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, heldWakeWhere, socketAwaitingWhere, } from "./wakeSql.js";
|
|
28
28
|
import { readTurnCount } from "./turnCount.js";
|
|
29
29
|
import { leadSessionEnded } from "./leadState.js";
|
|
30
30
|
import { heldReasonLabel } from "./heldLabel.js";
|
|
@@ -390,14 +390,11 @@ function reportMigrationResult(result) {
|
|
|
390
390
|
console.error(`! ${warning}`);
|
|
391
391
|
}
|
|
392
392
|
async function cmdLead(argv) {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const noDashboard = argv.includes("--no-dashboard");
|
|
399
|
-
const detach = argv.includes("--detach");
|
|
400
|
-
const path = argv.find((a) => !a.startsWith("--"));
|
|
393
|
+
const parsed = parseArgs(argv, { flags: ["--no-dashboard", "--detach"] });
|
|
394
|
+
rejectUnknownFlags("lead", parsed, "--no-dashboard and --detach");
|
|
395
|
+
const noDashboard = parsed.flags.has("--no-dashboard");
|
|
396
|
+
const detach = parsed.flags.has("--detach");
|
|
397
|
+
const path = parsed.positional[0];
|
|
401
398
|
let registrationNotice = null;
|
|
402
399
|
try {
|
|
403
400
|
const project = resolveProjectForWrite("hive lead", path, (text) => {
|
|
@@ -1163,6 +1160,8 @@ function profileSizeAdvisory(name, file) {
|
|
|
1163
1160
|
}
|
|
1164
1161
|
function cmdProfile(argv) {
|
|
1165
1162
|
const [sub, ...rest] = argv;
|
|
1163
|
+
if (sub?.startsWith("-"))
|
|
1164
|
+
rejectUnknownFlags("profile", parseArgs(argv, {}), "none");
|
|
1166
1165
|
try {
|
|
1167
1166
|
switch (sub) {
|
|
1168
1167
|
case undefined:
|
|
@@ -1340,12 +1339,9 @@ function openDashboardOnce(project, dashboardFile) {
|
|
|
1340
1339
|
}
|
|
1341
1340
|
}
|
|
1342
1341
|
function cmdAttach(argv) {
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
process.exit(1);
|
|
1347
|
-
}
|
|
1348
|
-
const path = argv.find((a) => !a.startsWith("--"));
|
|
1342
|
+
const parsed = parseArgs(argv, {});
|
|
1343
|
+
rejectUnknownFlags("attach", parsed, "none");
|
|
1344
|
+
const path = parsed.positional[0];
|
|
1349
1345
|
const project = resolveProjectForWrite("hive attach", path);
|
|
1350
1346
|
maybeOpenDashboard(project, !!loadProjectYml(project.path).config?.dashboard);
|
|
1351
1347
|
const session = sessionName();
|
|
@@ -1496,8 +1492,7 @@ const leadPaneExitedHookArmed = () => globalHooks(LEAD_PANE_EXITED_HOOK).some((e
|
|
|
1496
1492
|
function cmdLeadPaneExited(argv) {
|
|
1497
1493
|
try {
|
|
1498
1494
|
const parsed = parseArgs(argv, {});
|
|
1499
|
-
|
|
1500
|
-
return;
|
|
1495
|
+
rejectUnknownFlags("lead-pane-exited", parsed, "none");
|
|
1501
1496
|
const pane = parsed.positional[0];
|
|
1502
1497
|
if (!pane)
|
|
1503
1498
|
return;
|
|
@@ -2315,14 +2310,13 @@ function reportEffectiveConfig(loaded) {
|
|
|
2315
2310
|
: `${autoAttach} (default; set with \`hive setup --auto-attach\`; source: built-in)`);
|
|
2316
2311
|
}
|
|
2317
2312
|
function cmdDoctor(argv) {
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
}
|
|
2313
|
+
const parsed = parseArgs(argv, { flags: ["--strict", "--verbose"] });
|
|
2314
|
+
const unexpected = argv.find((arg) => arg !== "--strict" && arg !== "--verbose");
|
|
2315
|
+
if (unexpected !== undefined)
|
|
2316
|
+
parsed.unknown = [unexpected];
|
|
2317
|
+
rejectUnknownFlags("doctor", parsed, "--strict and --verbose");
|
|
2318
|
+
const strict = parsed.flags.has("--strict");
|
|
2319
|
+
doctorVerbose = parsed.flags.has("--verbose");
|
|
2326
2320
|
reportMigrationResult(migrateLegacyConfig());
|
|
2327
2321
|
let failures = 0;
|
|
2328
2322
|
const fail = (label, ...lines) => {
|
|
@@ -2801,13 +2795,13 @@ function cmdStatusline() {
|
|
|
2801
2795
|
const held = db
|
|
2802
2796
|
.prepare(`WITH chosen AS (
|
|
2803
2797
|
SELECT held_reason, first_held_at FROM wakes
|
|
2804
|
-
WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND
|
|
2798
|
+
WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND ${heldWakeWhere()}
|
|
2805
2799
|
ORDER BY (held_reason LIKE ?) DESC, (held_reason LIKE ?) ASC,
|
|
2806
2800
|
first_held_at IS NULL ASC, first_held_at ASC
|
|
2807
2801
|
LIMIT 1
|
|
2808
2802
|
)
|
|
2809
2803
|
SELECT
|
|
2810
|
-
(SELECT COUNT(*) FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND
|
|
2804
|
+
(SELECT COUNT(*) FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND ${heldWakeWhere()}) AS n,
|
|
2811
2805
|
(SELECT held_reason FROM chosen) AS held_reason,
|
|
2812
2806
|
(SELECT first_held_at FROM chosen) AS first_held_at`)
|
|
2813
2807
|
.get(project.id, `${HELD_REASON_UNSUBMITTED_INPUT_PREFIX}%`, `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}%`, project.id);
|
|
@@ -3256,11 +3250,26 @@ function requireFlagValues(command, parsed) {
|
|
|
3256
3250
|
process.exit(1);
|
|
3257
3251
|
}
|
|
3258
3252
|
function cmdTodos(argv) {
|
|
3253
|
+
const parsed = parseArgs(argv, { flags: ["--all"], valued: ["--status", "--tag"] });
|
|
3254
|
+
for (const flag of ["--status", "--tag"]) {
|
|
3255
|
+
const index = argv.indexOf(flag);
|
|
3256
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
3257
|
+
if (index === -1 || (value !== undefined && !value.startsWith("--")))
|
|
3258
|
+
continue;
|
|
3259
|
+
if (flag === "--status") {
|
|
3260
|
+
console.log(`Usage: hive todos --status <s> where <s> is one of: ${TODO_STATUSES.join(", ")}`);
|
|
3261
|
+
}
|
|
3262
|
+
else {
|
|
3263
|
+
console.log("Usage: hive todos --tag <t>");
|
|
3264
|
+
}
|
|
3265
|
+
process.exit(1);
|
|
3266
|
+
}
|
|
3267
|
+
rejectUnknownFlags("todos", parsed, "--all, --status <status>, --tag <tag>");
|
|
3268
|
+
requireFlagValues("todos", parsed);
|
|
3259
3269
|
const project = resolveProjectForRead("todos");
|
|
3260
|
-
const all =
|
|
3261
|
-
const
|
|
3262
|
-
|
|
3263
|
-
if (statusIdx !== -1 && (status === undefined || status.startsWith("--"))) {
|
|
3270
|
+
const all = parsed.flags.has("--all");
|
|
3271
|
+
const status = parsed.values.get("--status");
|
|
3272
|
+
if (parsed.values.has("--status") && status === undefined) {
|
|
3264
3273
|
console.log(`Usage: hive todos --status <s> where <s> is one of: ${TODO_STATUSES.join(", ")}`);
|
|
3265
3274
|
process.exit(1);
|
|
3266
3275
|
}
|
|
@@ -3268,9 +3277,8 @@ function cmdTodos(argv) {
|
|
|
3268
3277
|
console.log(`Unknown status "${status}". Valid: ${TODO_STATUSES.join(", ")}`);
|
|
3269
3278
|
process.exit(1);
|
|
3270
3279
|
}
|
|
3271
|
-
const
|
|
3272
|
-
|
|
3273
|
-
if (tagIdx !== -1 && (tag === undefined || tag.startsWith("--"))) {
|
|
3280
|
+
const tag = parsed.values.get("--tag");
|
|
3281
|
+
if (parsed.values.has("--tag") && tag === undefined) {
|
|
3274
3282
|
console.log("Usage: hive todos --tag <t>");
|
|
3275
3283
|
process.exit(1);
|
|
3276
3284
|
}
|
|
@@ -3300,8 +3308,10 @@ function cmdTodos(argv) {
|
|
|
3300
3308
|
}
|
|
3301
3309
|
}
|
|
3302
3310
|
function cmdTodo(argv) {
|
|
3311
|
+
const parsed = parseArgs(argv, {});
|
|
3312
|
+
rejectUnknownFlags("todo", parsed, "none");
|
|
3303
3313
|
const project = resolveProjectForRead("todo");
|
|
3304
|
-
const id = Number(
|
|
3314
|
+
const id = Number(parsed.positional[0]);
|
|
3305
3315
|
if (!Number.isInteger(id)) {
|
|
3306
3316
|
console.log("Usage: hive todo <id> (run inside the project)");
|
|
3307
3317
|
process.exit(1);
|
|
@@ -3505,6 +3515,8 @@ let rest = args.slice(1);
|
|
|
3505
3515
|
if (command === "--help" || command === "-h" || command === "help")
|
|
3506
3516
|
usage();
|
|
3507
3517
|
if (command === "--version" || command === "-v") {
|
|
3518
|
+
const parsed = parseArgs(rest, { flags: ["--check"] });
|
|
3519
|
+
rejectUnknownFlags("version", parsed, "--check");
|
|
3508
3520
|
const { line, drift } = versionInfo();
|
|
3509
3521
|
console.log(line);
|
|
3510
3522
|
if (drift)
|
|
@@ -3584,6 +3596,7 @@ try {
|
|
|
3584
3596
|
cmdHide(rest);
|
|
3585
3597
|
break;
|
|
3586
3598
|
case "status":
|
|
3599
|
+
rejectUnknownFlags("status", parseArgs(rest, {}), "none");
|
|
3587
3600
|
cmdStatus();
|
|
3588
3601
|
break;
|
|
3589
3602
|
case "portfolio":
|
|
@@ -3608,6 +3621,7 @@ try {
|
|
|
3608
3621
|
cmdDoctor(rest);
|
|
3609
3622
|
break;
|
|
3610
3623
|
case "pads":
|
|
3624
|
+
rejectUnknownFlags("pads", parseArgs(rest, {}), "none");
|
|
3611
3625
|
cmdPads();
|
|
3612
3626
|
break;
|
|
3613
3627
|
case "pad":
|
|
@@ -3626,6 +3640,7 @@ try {
|
|
|
3626
3640
|
cmdAnswer(rest);
|
|
3627
3641
|
break;
|
|
3628
3642
|
case "backups":
|
|
3643
|
+
rejectUnknownFlags("backups", parseArgs(rest, {}), "none");
|
|
3629
3644
|
cmdBackups();
|
|
3630
3645
|
break;
|
|
3631
3646
|
case "restore":
|
|
@@ -3634,19 +3649,29 @@ try {
|
|
|
3634
3649
|
case "project":
|
|
3635
3650
|
await cmdProject(rest);
|
|
3636
3651
|
break;
|
|
3637
|
-
case "runbook":
|
|
3638
|
-
|
|
3652
|
+
case "runbook": {
|
|
3653
|
+
const parsed = parseArgs(rest, {});
|
|
3654
|
+
rejectUnknownFlags("runbook", parsed, "none");
|
|
3655
|
+
cmdRunbook(parsed.positional[0]);
|
|
3639
3656
|
break;
|
|
3640
|
-
|
|
3641
|
-
|
|
3657
|
+
}
|
|
3658
|
+
case "posture": {
|
|
3659
|
+
const parsed = parseArgs(rest, {});
|
|
3660
|
+
rejectUnknownFlags("posture", parsed, "none");
|
|
3661
|
+
cmdPosture(parsed.positional[0]);
|
|
3642
3662
|
break;
|
|
3663
|
+
}
|
|
3643
3664
|
case "profile":
|
|
3644
3665
|
cmdProfile(rest);
|
|
3645
3666
|
break;
|
|
3646
|
-
case "kickoff":
|
|
3667
|
+
case "kickoff": {
|
|
3668
|
+
const parsed = parseArgs(rest, { flags: ["--explain", "--codex"] });
|
|
3669
|
+
rejectUnknownFlags("kickoff", parsed, "--explain, --codex");
|
|
3647
3670
|
await (await import("./kickoff.js")).runKickoff(rest);
|
|
3648
3671
|
break;
|
|
3672
|
+
}
|
|
3649
3673
|
case "statusline":
|
|
3674
|
+
rejectUnknownFlags("statusline", parseArgs(rest, {}), "none");
|
|
3650
3675
|
cmdStatusline();
|
|
3651
3676
|
break;
|
|
3652
3677
|
default:
|
package/dist/crew.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
1
2
|
import { db } from "./db.js";
|
|
2
3
|
import { isLinkedWorktree } from "./context.js";
|
|
3
|
-
import {
|
|
4
|
+
import { finishedIdle } from "./firstPrompt.js";
|
|
4
5
|
import { heldReasonLabel } from "./heldLabel.js";
|
|
5
6
|
import { harnessFor, paneClassifierFor } from "./harnesses.js";
|
|
6
7
|
import { NEEDS_HUMAN_TAG } from "./portfolio.js";
|
|
@@ -29,7 +30,7 @@ export function crewStoreProblem(database = db) {
|
|
|
29
30
|
function isoUtc(value) {
|
|
30
31
|
if (!value)
|
|
31
32
|
return null;
|
|
32
|
-
const date = new Date(
|
|
33
|
+
const date = new Date(parseStoreTimestamp(value));
|
|
33
34
|
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
34
35
|
}
|
|
35
36
|
const WAKE_LABEL_MAX = 60;
|
|
@@ -452,7 +453,7 @@ export function collectCrew(project, now) {
|
|
|
452
453
|
age_seconds: Math.max(0, Math.floor((now.getTime() - Date.parse(isoUtc(row.created_at) ?? "")) / 1000) || 0),
|
|
453
454
|
activity,
|
|
454
455
|
context_fill: kind ? readContextFill(kind, worker) : null,
|
|
455
|
-
your_turn:
|
|
456
|
+
your_turn: finishedIdle(row),
|
|
456
457
|
commits_ahead: commitsByCwd.get(row.cwd) ?? null,
|
|
457
458
|
},
|
|
458
459
|
});
|
package/dist/dashboard.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
|
|
|
4
4
|
import { db } from "./db.js";
|
|
5
5
|
import { ACTIVE_TODO_SQL, NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
|
|
6
6
|
import { getProject } from "./context.js";
|
|
7
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
7
8
|
export const describeVisibility = (visibility) => visibility === null ? "running" : visibility === "window" ? "own window" : visibility;
|
|
8
9
|
export function processCounts(procs) {
|
|
9
10
|
return {
|
|
@@ -13,7 +14,7 @@ export function processCounts(procs) {
|
|
|
13
14
|
notStarted: procs.filter((p) => !p.running).length,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
|
-
import {
|
|
17
|
+
import { finishedIdle } from "./firstPrompt.js";
|
|
17
18
|
import { SLUG_MAX_LEN, cutToUnitBudget } from "./slug.js";
|
|
18
19
|
export const TODO_CAP = 100;
|
|
19
20
|
export const AGENT_CAP = 50;
|
|
@@ -55,7 +56,7 @@ function formatDate(d) {
|
|
|
55
56
|
export function formatLocal(utc) {
|
|
56
57
|
if (!utc)
|
|
57
58
|
return "-";
|
|
58
|
-
const d = new Date(
|
|
59
|
+
const d = new Date(parseStoreTimestamp(utc));
|
|
59
60
|
if (Number.isNaN(d.getTime()))
|
|
60
61
|
return utc;
|
|
61
62
|
return formatDate(d);
|
|
@@ -70,7 +71,7 @@ function timeEl(utc) {
|
|
|
70
71
|
function shortWhenEl(utc) {
|
|
71
72
|
if (!utc)
|
|
72
73
|
return '<span class="muted">-</span>';
|
|
73
|
-
const d = new Date(
|
|
74
|
+
const d = new Date(parseStoreTimestamp(utc));
|
|
74
75
|
if (Number.isNaN(d.getTime()))
|
|
75
76
|
return `<span class="time">${escapeHtml(utc)}</span>`;
|
|
76
77
|
const local = formatDate(d);
|
|
@@ -97,7 +98,7 @@ function agentStatusLevel(state) {
|
|
|
97
98
|
return "warn";
|
|
98
99
|
}
|
|
99
100
|
function agentStateBadge(a) {
|
|
100
|
-
return
|
|
101
|
+
return a.agent_state === "idle" && !finishedIdle(a)
|
|
101
102
|
? statusBadge("live", "idle (no assignment yet)")
|
|
102
103
|
: statusBadge(agentStatusLevel(a.agent_state), a.agent_state);
|
|
103
104
|
}
|
|
@@ -448,7 +449,7 @@ function renderWakesSection(index, projectId) {
|
|
|
448
449
|
return section(index, "wakes", "Wakes", `${all.length} pending`, "bell", String(shown.length), body);
|
|
449
450
|
}
|
|
450
451
|
function questionAge(createdAt) {
|
|
451
|
-
const timestamp =
|
|
452
|
+
const timestamp = parseStoreTimestamp(createdAt);
|
|
452
453
|
if (!Number.isFinite(timestamp))
|
|
453
454
|
return "unknown";
|
|
454
455
|
const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
|
package/dist/etime.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function parseEtimeSeconds(value) {
|
|
2
|
+
const dayForm = /^(\d+)-(\d{1,2}):(\d{2}):(\d{2})$/.exec(value);
|
|
3
|
+
const digitForm = /^(\d+)$/.exec(value);
|
|
4
|
+
const fields = dayForm ? dayForm.slice(1).map(Number) : value.split(":").map(Number);
|
|
5
|
+
if (!dayForm &&
|
|
6
|
+
!digitForm &&
|
|
7
|
+
((fields.length === 2 && !/^\d+:\d+$/.test(value)) ||
|
|
8
|
+
(fields.length === 3 && !/^\d+:\d+:\d+$/.test(value)) ||
|
|
9
|
+
(fields.length !== 2 && fields.length !== 3))) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (fields.some((field) => !Number.isSafeInteger(field) || field < 0))
|
|
13
|
+
return null;
|
|
14
|
+
let total;
|
|
15
|
+
if (digitForm) {
|
|
16
|
+
total = fields[0];
|
|
17
|
+
}
|
|
18
|
+
else if (dayForm) {
|
|
19
|
+
const [days, hours, minutes, seconds] = fields;
|
|
20
|
+
if (hours > 23 || minutes > 59 || seconds > 59)
|
|
21
|
+
return null;
|
|
22
|
+
total = ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
|
|
23
|
+
}
|
|
24
|
+
else if (fields.length === 2) {
|
|
25
|
+
const [minutes, seconds] = fields;
|
|
26
|
+
if (seconds > 59)
|
|
27
|
+
return null;
|
|
28
|
+
total = minutes * 60 + seconds;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const [hours, minutes, seconds] = fields;
|
|
32
|
+
if (minutes > 59 || seconds > 59)
|
|
33
|
+
return null;
|
|
34
|
+
total = (hours * 60 + minutes) * 60 + seconds;
|
|
35
|
+
}
|
|
36
|
+
return Number.isSafeInteger(total) ? total : null;
|
|
37
|
+
}
|
package/dist/firstPrompt.js
CHANGED
|
@@ -4,3 +4,9 @@ export function awaitingFirstPrompt(row) {
|
|
|
4
4
|
export function awaitingFirstPromptSql(alias) {
|
|
5
5
|
return `${alias}.resumed_at != ''`;
|
|
6
6
|
}
|
|
7
|
+
export function finishedIdle(row) {
|
|
8
|
+
return row.agent_state === "idle" && !awaitingFirstPrompt(row);
|
|
9
|
+
}
|
|
10
|
+
export function finishedIdleSql(alias) {
|
|
11
|
+
return `${alias}.agent_state = 'idle' AND NOT (${awaitingFirstPromptSql(alias)})`;
|
|
12
|
+
}
|
package/dist/heldLabel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, isLeadRowClosedHold, isUnclassifiablePaneHold, isUnsubmittedInputHold, wasHeldForPaneIdentity, } from "./
|
|
1
|
+
import { HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, isLeadRowClosedHold, isUnclassifiablePaneHold, isUnsubmittedInputHold, wasHeldForPaneIdentity, } from "./wakeSql.js";
|
|
2
2
|
// test/docs.test.mjs fails if a label here is missing from docs/install.md. It checks
|
|
3
3
|
// presence only: docs/install.md also counts the set in prose, and nothing guards that number.
|
|
4
4
|
export const HELD_REASON_LABELS = ["typing", "talking", "needs you", "blocked"];
|
package/dist/leadHandoff.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { db } from "./db.js";
|
|
3
|
+
import { MESSAGE_RETENTION, MESSAGE_STATUS } from "./leadMessage.js";
|
|
4
|
+
import { HELD_REASON_HANDOFF, LOG_RETENTION, handoffSocketInFlightWhere, pendingOneShotWhere, } from "./wakeSql.js";
|
|
3
5
|
import { harnessFor, paneClassifierFor } from "./harnesses.js";
|
|
4
|
-
import { casHandoff,
|
|
6
|
+
import { casHandoff, handoffHoldsAutomation, nowSql, olderThan, PASS_TIMEOUT_SECONDS, PRE_RESPAWN_STATES, passPolicy, quotedPolicy, readActiveHandoff, readHandoff, STOP_HOLD_CAP_SECONDS, SUCCESSOR_NOT_READY, } from "./leadHandoffState.js";
|
|
5
7
|
import { holdsHumanInput, paneInCopyMode, rowOwnership, tmuxSocketPath } from "./tmux.js";
|
|
6
8
|
import { readLeadSafetySnapshot } from "./leadState.js";
|
|
7
9
|
import { readTurnCount } from "./turnCount.js";
|
|
@@ -94,7 +96,7 @@ const failTransaction = db.transaction((id, failure, from, where) => {
|
|
|
94
96
|
if (!casHandoff(id, from, { state: "failed", failure, owner_token: null, owner_pid: null }, where))
|
|
95
97
|
return false;
|
|
96
98
|
db.prepare(`UPDATE wakes SET cancelled_at = datetime('now'), held_reason = ?
|
|
97
|
-
WHERE id = (SELECT request_wake_id FROM lead_handoffs WHERE id = ?) AND
|
|
99
|
+
WHERE id = (SELECT request_wake_id FROM lead_handoffs WHERE id = ?) AND ${pendingOneShotWhere()}`).run(`lead handoff #${id} failed: ${failure}`, id);
|
|
98
100
|
return true;
|
|
99
101
|
});
|
|
100
102
|
export function failHandoff(id, failure, from = PRE_RESPAWN_STATES, where = {}) {
|
|
@@ -158,13 +160,12 @@ export function requestBody(row, turns) {
|
|
|
158
160
|
// Automated deliveries to the lead other than the handoff's own request wakes.
|
|
159
161
|
export function automationBlocker(lead, seconds) {
|
|
160
162
|
const inFlight = db
|
|
161
|
-
.prepare(`SELECT 1 AS hit FROM agent_messages WHERE to_agent_id = ? AND delivery_status IN (
|
|
162
|
-
AND created_at >= datetime('now',
|
|
163
|
+
.prepare(`SELECT 1 AS hit FROM agent_messages WHERE to_agent_id = ? AND delivery_status IN (?, ?)
|
|
164
|
+
AND created_at >= datetime('now', ?)
|
|
163
165
|
UNION ALL
|
|
164
|
-
SELECT 1 FROM wakes WHERE deliver_actor = ? AND body NOT LIKE ? AND
|
|
165
|
-
AND delivery_method LIKE 'socket%' AND confirmed_at IS NULL AND socket_attempt_at >= datetime('now', '-7 days')
|
|
166
|
+
SELECT 1 FROM wakes WHERE deliver_actor = ? AND body NOT LIKE ? AND ${handoffSocketInFlightWhere()}
|
|
166
167
|
LIMIT 1`)
|
|
167
|
-
.get(lead.id, lead.actor_id, `${REQUEST_BODY_PREFIX}
|
|
168
|
+
.get(lead.id, MESSAGE_STATUS.socketPending, MESSAGE_STATUS.fallbackClaimed, MESSAGE_RETENTION, lead.actor_id, `${REQUEST_BODY_PREFIX}%`, LOG_RETENTION);
|
|
168
169
|
if (inFlight !== undefined)
|
|
169
170
|
return "a delivery to the lead is still in flight";
|
|
170
171
|
if (seconds === null)
|
|
@@ -204,7 +205,7 @@ const requestTransaction = db.transaction((row, lead, turns) => {
|
|
|
204
205
|
const waiting = current.request_wake_id === null
|
|
205
206
|
? undefined
|
|
206
207
|
: db
|
|
207
|
-
.prepare(
|
|
208
|
+
.prepare(`SELECT id FROM wakes WHERE id = ? AND due_at IS NULL AND ${pendingOneShotWhere()}`)
|
|
208
209
|
.get(current.request_wake_id);
|
|
209
210
|
let wakeId;
|
|
210
211
|
if (waiting !== undefined) {
|
|
@@ -435,12 +436,12 @@ export function handoffInjection(leadActor) {
|
|
|
435
436
|
.prepare("SELECT name, agent_state FROM agents WHERE project_id = ? AND kind = 'agent' AND status = 'running' ORDER BY id")
|
|
436
437
|
.all(lead.project_id);
|
|
437
438
|
const pending = db
|
|
438
|
-
.prepare(
|
|
439
|
+
.prepare(`SELECT COUNT(*) AS n FROM wakes WHERE deliver_actor = ? AND ${pendingOneShotWhere()}`)
|
|
439
440
|
.get(lead.actor_id).n;
|
|
440
441
|
const heldWakes = db.prepare("SELECT COUNT(*) AS n FROM wakes WHERE deliver_actor = ? AND held_reason = ? AND cancelled_at IS NULL").get(lead.actor_id, HELD_REASON_HANDOFF).n;
|
|
441
442
|
const heldMessages = db
|
|
442
|
-
.prepare("SELECT COUNT(*) AS n FROM agent_messages WHERE to_agent_id = ? AND delivery_status =
|
|
443
|
-
.get(lead.id).n;
|
|
443
|
+
.prepare("SELECT COUNT(*) AS n FROM agent_messages WHERE to_agent_id = ? AND delivery_status = ? AND created_at >= datetime('now', ?)")
|
|
444
|
+
.get(lead.id, MESSAGE_STATUS.fallbackPending, MESSAGE_RETENTION).n;
|
|
444
445
|
const lines = [`[hive] LEAD HANDOFF: pad "${HANDOFF_PAD_NAME}" #${pad.id} revision ${pad.revision}, written ${pad.updated_at} UTC.`];
|
|
445
446
|
{
|
|
446
447
|
lines.push(`Handed off by session ${row.predecessor_session_id} at turn ${row.predecessor_turns ?? "?"} (${row.reason}, pass ${row.pass})` +
|
package/dist/leadHandoffCli.js
CHANGED
|
@@ -21,13 +21,28 @@ function flags(argv, names) {
|
|
|
21
21
|
const out = {};
|
|
22
22
|
for (let i = 0; i < argv.length; i++) {
|
|
23
23
|
const name = argv[i].replace(/^--/, "");
|
|
24
|
-
if (!argv[i].startsWith("--") || !names.includes(name) || argv[i + 1] === undefined)
|
|
24
|
+
if (!argv[i].startsWith("--") || !names.includes(name) || argv[i + 1] === undefined) {
|
|
25
25
|
return `unexpected argument "${argv[i]}"`;
|
|
26
|
+
}
|
|
26
27
|
out[name] = argv[++i];
|
|
27
28
|
}
|
|
28
29
|
const absent = names.filter((n) => out[n] === undefined);
|
|
29
30
|
return absent.length > 0 ? `missing --${absent.join(", --")}` : out;
|
|
30
31
|
}
|
|
32
|
+
function rejectUnknownFlag(argv, command, names, flagsText) {
|
|
33
|
+
const known = new Set(names.map((name) => `--${name}`));
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
if (known.has(argv[i])) {
|
|
36
|
+
i++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (argv[i].startsWith("-")) {
|
|
40
|
+
console.error(`hive ${command}: unknown argument "${argv[i]}". Flags are ${flagsText}.`);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
31
46
|
const intFlag = (value) => (/^\d+$/.test(value) ? Number(value) : null);
|
|
32
47
|
function runningLeadById(id) {
|
|
33
48
|
return db
|
|
@@ -36,6 +51,8 @@ function runningLeadById(id) {
|
|
|
36
51
|
.get(id);
|
|
37
52
|
}
|
|
38
53
|
export async function cmdLeadHandoff(argv) {
|
|
54
|
+
if (rejectUnknownFlag(argv, "lead-handoff", ["request", "pad", "revision"], "--request <id>, --pad <id>, --revision <n>"))
|
|
55
|
+
return 1;
|
|
39
56
|
const refuse = (why) => {
|
|
40
57
|
console.error(`hive lead-handoff: ${why}. This session stays as it is.`);
|
|
41
58
|
return 1;
|
|
@@ -152,6 +169,8 @@ const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
152
169
|
// `hive lead-handoff-grace`: the one detached owner of an attempt. It never outlives a changed
|
|
153
170
|
// attempt or token, and after it claims the respawn it never repeats the kill.
|
|
154
171
|
export async function runHandoffGrace(argv) {
|
|
172
|
+
if (rejectUnknownFlag(argv, "lead-handoff-grace", ["request", "attempt", "token"], "--request <id>, --attempt <n>, --token <token>"))
|
|
173
|
+
return 1;
|
|
155
174
|
try {
|
|
156
175
|
return await graceLoop(argv);
|
|
157
176
|
}
|
|
@@ -245,6 +264,8 @@ async function graceLoop(argv) {
|
|
|
245
264
|
// `hive lead-handoff-start`: runs in the respawned pane before Claude. It publishes the successor's
|
|
246
265
|
// pid on the same lead row by CAS against the predecessor, and only then lets the shell exec Claude.
|
|
247
266
|
export async function runHandoffStart(argv) {
|
|
267
|
+
if (rejectUnknownFlag(argv, "lead-handoff-start", ["request", "attempt", "token"], "--request <id>, --attempt <n>, --token <token>"))
|
|
268
|
+
return 1;
|
|
248
269
|
const parsed = flags(argv, ["request", "attempt", "token"]);
|
|
249
270
|
const refuse = (why, id) => {
|
|
250
271
|
if (id !== undefined) {
|
package/dist/leadHandoffState.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { db } from "./db.js";
|
|
2
|
+
export { HELD_REASON_HANDOFF } from "./wakeSql.js";
|
|
2
3
|
export const PRE_RESPAWN_STATES = ["pending", "wind_down", "requested", "grace", "postponed"];
|
|
3
4
|
export const PASS_TIMEOUT_SECONDS = 10 * 60;
|
|
4
5
|
export const STOP_HOLD_CAP_SECONDS = 15 * 60;
|
|
@@ -53,8 +54,6 @@ export function readHandoffGate(leadAgentId) {
|
|
|
53
54
|
? null
|
|
54
55
|
: { requestId: row.id, requestWakeId: row.request_wake_id, holdAutomation: handoffHoldsAutomation(row) };
|
|
55
56
|
}
|
|
56
|
-
export const HELD_REASON_HANDOFF = "the lead is handing off to a fresh session; held through the wind-down and the successor's boot, and delivered to " +
|
|
57
|
-
"the lead row once the successor completes its first turn, or when the handoff fails or releases its hold";
|
|
58
57
|
// A wake addressed to a lead row is held while that lead's handoff holds automation, except the
|
|
59
58
|
// current request wake itself, which still passes every human-input and ownership guard.
|
|
60
59
|
export function handoffHoldsWake(wakeId, deliverRowId) {
|
package/dist/leadMessage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { db } from "./db.js";
|
|
2
|
+
import { DELIVERY_METHOD } from "./claudeWake.js";
|
|
2
3
|
import { cutToUnitBudget, flatten } from "./slug.js";
|
|
3
4
|
// 300 sits in the empty band between the two measured populations (todo 475): the largest lead-bound
|
|
4
5
|
// message anyone has argued must arrive whole is 143 bytes, and the smallest real worker report is 440.
|
|
@@ -14,6 +15,18 @@ export const MESSAGE_RETENTION = "-7 days";
|
|
|
14
15
|
// direction. Far above any observed rate, and lower than agent_state_log's 20,000 because a row here
|
|
15
16
|
// holds unbounded message text rather than a hook payload.
|
|
16
17
|
export const MESSAGE_MAX_ROWS = 5_000;
|
|
18
|
+
export const MESSAGE_STATUS = {
|
|
19
|
+
socketPending: "socket-pending",
|
|
20
|
+
fallbackPending: "fallback-pending",
|
|
21
|
+
fallbackClaimed: "fallback-claimed",
|
|
22
|
+
complete: "complete",
|
|
23
|
+
failed: "failed",
|
|
24
|
+
};
|
|
25
|
+
export const MESSAGE_METHOD = {
|
|
26
|
+
socket: DELIVERY_METHOD.socket,
|
|
27
|
+
pty: DELIVERY_METHOD.pty,
|
|
28
|
+
ptyAfterSocketTimeout: DELIVERY_METHOD.ptyAfterSocketTimeout,
|
|
29
|
+
};
|
|
17
30
|
export const DELIVERY_FIELDS = [
|
|
18
31
|
"delivery_status",
|
|
19
32
|
"delivery_method",
|