@cmgmyr/hive 1.11.6 → 1.11.8

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.
@@ -1 +1 @@
1
- {"version":"1.11.6","sha":"b4e9fb0","dirty":false,"build_id":"fb0debbc0e5e030650660c10505b25c26e60160403f384ce900920146e639961"}
1
+ {"version":"1.11.8","sha":"500220b","dirty":false,"build_id":"1286494bf19194350611bfbf75688ec5a3c7e4b033b7c8dc84a101f57528dcbb"}
@@ -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 SOCKET_WRITE_BOUND_MS = 1000;
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 claudeWakeFrame(senderAddress, text) {
10
- const content = `<cross-session-message from="${senderAddress}" from-name="hive">\n${text}\n${CROSS_SESSION_CLOSE}`;
11
- return JSON.stringify({ type: "user", from: senderAddress, message: { role: "user", content } }) + "\n";
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 { socketAwaitingWhere, dashboardFileContained, describeStall, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, } from "./scheduler.js";
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 unknownFlag = argv.find((a) => a.startsWith("--") && a !== "--no-dashboard" && a !== "--detach");
394
- if (unknownFlag !== undefined) {
395
- console.error(`hive lead: unknown flag "${unknownFlag}". Flags are --no-dashboard and --detach.`);
396
- process.exit(1);
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 unknownFlag = argv.find((a) => a.startsWith("--"));
1344
- if (unknownFlag !== undefined) {
1345
- console.error(`hive attach: unknown flag "${unknownFlag}". hive attach takes no flags.`);
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
- if (parsed.unknown.length > 0)
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 strict = argv.includes("--strict");
2319
- doctorVerbose = argv.includes("--verbose");
2320
- const KNOWN_DOCTOR_FLAGS = new Set(["--strict", "--verbose"]);
2321
- const unknown = argv.find((a) => !KNOWN_DOCTOR_FLAGS.has(a));
2322
- if (unknown !== undefined) {
2323
- console.error(`hive doctor: unknown argument "${unknown}". Flags are --strict and --verbose.`);
2324
- process.exit(1);
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 held_at IS NOT NULL
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 held_at IS NOT NULL) AS n,
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 = argv.includes("--all");
3261
- const statusIdx = argv.indexOf("--status");
3262
- const status = flagValue(argv, "--status");
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 tagIdx = argv.indexOf("--tag");
3272
- const tag = flagValue(argv, "--tag");
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(argv.find((a) => !a.startsWith("--")));
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
- cmdRunbook(rest[0]);
3652
+ case "runbook": {
3653
+ const parsed = parseArgs(rest, {});
3654
+ rejectUnknownFlags("runbook", parsed, "none");
3655
+ cmdRunbook(parsed.positional[0]);
3639
3656
  break;
3640
- case "posture":
3641
- cmdPosture(rest[0]);
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 { awaitingFirstPrompt } from "./firstPrompt.js";
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(/(Z|[+-]\d\d:?\d\d)$/.test(value) ? value : `${value.replace(" ", "T")}Z`);
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: row.agent_state === "idle" && !awaitingFirstPrompt(row),
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 { awaitingFirstPrompt } from "./firstPrompt.js";
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(`${utc.replace(" ", "T")}Z`);
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(`${utc.replace(" ", "T")}Z`);
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 awaitingFirstPrompt(a) && a.agent_state === "idle"
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 = Date.parse(`${createdAt.replace(" ", "T")}Z`);
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
+ }
@@ -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 "./scheduler.js";
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/index.js CHANGED
@@ -15,11 +15,12 @@ import { registerQueenAudit } from "./tools/queenAudit.js";
15
15
  import { registerPrompts } from "./prompts.js";
16
16
  import { schedulerIntervalMs, startScheduler } from "./scheduler.js";
17
17
  import { enforceStrictInput } from "./strictInput.js";
18
+ import { hiveVersion } from "./version.js";
18
19
  migrate();
19
20
  const SCOPE_DISCIPLINE = isQueenLead()
20
21
  ? "Scope discipline: you are the queen, the lead of hive's queen home. Your own project is the working directory's. You may read any registered project by passing its project_id. Into another project you may write only todo_create and todo_comment, plus wakes and agent_send text addressed to that project's running lead; every other write there refuses with QUEEN_CROSS_PROJECT_WRITE_REFUSED. See help(topic=\"projects\")."
21
22
  : "Scope discipline: all state is project-scoped, and the current project is the working directory's. An empty result means there is nothing IN THIS PROJECT; report that and stop. Never browse other projects' state (project_id overrides, project_select) unless the user explicitly names another project and asks for it.";
22
- const server = enforceStrictInput(new McpServer({ name: "hive", version: "0.1.0" }, {
23
+ const server = enforceStrictInput(new McpServer({ name: "hive", version: hiveVersion() }, {
23
24
  instructions: `Hive gives Claude Code sessions shared, persistent state for coordinating work on a project: pads for plans and findings, todos for tracked work with blockers, kv for small shared values, leases for claiming shared state, agents for spawning and directing other sessions, and wake-ups for scheduled or idle-triggered check-ins. Search for these tools when a task needs to persist past this conversation, hand off to or watch another session, or track multi-step work with dependencies.
24
25
  Getting started:
25
26
  1. Call whoami to see your actor identity and effective project scope.
@@ -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, HELD_REASON_HANDOFF, handoffHoldsAutomation, nowSql, olderThan, PASS_TIMEOUT_SECONDS, PRE_RESPAWN_STATES, passPolicy, quotedPolicy, readActiveHandoff, readHandoff, STOP_HOLD_CAP_SECONDS, SUCCESSOR_NOT_READY, } from "./leadHandoffState.js";
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 fired_at IS NULL AND cancelled_at IS NULL`).run(`lead handoff #${id} failed: ${failure}`, id);
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 ('socket-pending', 'fallback-claimed')
162
- AND created_at >= datetime('now', '-7 days')
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 fired_at IS NOT NULL AND cancelled_at IS NULL
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("SELECT id FROM wakes WHERE id = ? AND due_at IS NULL AND fired_at IS NULL AND cancelled_at IS NULL")
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("SELECT COUNT(*) AS n FROM wakes WHERE deliver_actor = ? AND fired_at IS NULL AND cancelled_at IS NULL")
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 = 'fallback-pending' AND created_at >= datetime('now', '-7 days')")
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})` +
@@ -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) {
@@ -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) {
@@ -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",