@cmgmyr/hive 1.11.5 → 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.
Files changed (51) hide show
  1. package/dist/agentKinds.js +6 -0
  2. package/dist/agentRecords.js +98 -0
  3. package/dist/backup.js +1 -1
  4. package/dist/build-info.json +1 -1
  5. package/dist/claudeWake.js +52 -4
  6. package/dist/cli.js +69 -43
  7. package/dist/context.js +2 -2
  8. package/dist/crew.js +5 -4
  9. package/dist/dashboard.js +10 -12
  10. package/dist/db.js +1 -1
  11. package/dist/errors.js +3 -0
  12. package/dist/etime.js +37 -0
  13. package/dist/firstPrompt.js +6 -0
  14. package/dist/globalConfig.js +1 -1
  15. package/dist/heldLabel.js +1 -1
  16. package/dist/kickoff.js +2 -2
  17. package/dist/leadHandoff.js +12 -11
  18. package/dist/leadHandoffCli.js +22 -1
  19. package/dist/leadHandoffState.js +1 -2
  20. package/dist/leadLaunch.js +1 -1
  21. package/dist/leadMessage.js +13 -0
  22. package/dist/leadMessageDelivery.js +14 -74
  23. package/dist/leadState.js +3 -2
  24. package/dist/leadWatch.js +1 -1
  25. package/dist/leaseStore.js +54 -0
  26. package/dist/pads.js +70 -0
  27. package/dist/portfolio.js +7 -3
  28. package/dist/projectRemove.js +1 -1
  29. package/dist/projectYml.js +1 -1
  30. package/dist/ptys.js +2 -9
  31. package/dist/queenAudit.js +2 -18
  32. package/dist/queenAuditStore.js +20 -0
  33. package/dist/queenDashboard.js +2 -1
  34. package/dist/questions.js +1 -1
  35. package/dist/result.js +2 -3
  36. package/dist/scheduler.js +42 -166
  37. package/dist/spawn.js +2 -6
  38. package/dist/staleServers.js +4 -28
  39. package/dist/stateProvenance.js +1 -3
  40. package/dist/storeClock.js +4 -0
  41. package/dist/teardown.js +2 -1
  42. package/dist/todoScope.js +1 -0
  43. package/dist/todos.js +90 -0
  44. package/dist/tools/agents.js +4 -96
  45. package/dist/tools/leases.js +5 -25
  46. package/dist/tools/pads.js +3 -82
  47. package/dist/tools/todos.js +6 -89
  48. package/dist/tools/wakes.js +12 -88
  49. package/dist/wakeSql.js +135 -0
  50. package/dist/wakeTarget.js +76 -0
  51. package/package.json +1 -1
@@ -0,0 +1,6 @@
1
+ export const LEAD_KIND = "lead";
2
+ export const LEAD_ACTOR_PREFIX = `${LEAD_KIND}:`;
3
+ export const isLeadActorId = (actorId) => actorId.startsWith(LEAD_ACTOR_PREFIX);
4
+ export const mintLeadActorId = (agentId) => `${LEAD_ACTOR_PREFIX}${agentId}`;
5
+ export const LEAD_NAME = LEAD_KIND;
6
+ export const isReservedAgentName = (name) => name.toLowerCase() === LEAD_NAME;
@@ -0,0 +1,98 @@
1
+ import { db } from "./db.js";
2
+ import { LEAD_KIND } from "./agentKinds.js";
3
+ import { observationFailed, observeRowOwnership, ownershipLiveness, rowOwnership } from "./tmux.js";
4
+ export const CLOSED_ROW_ORDER = "(parked_at != '') DESC, closed_at DESC, id DESC";
5
+ export function getAgentRow(projectId, id, notFoundHint) {
6
+ const row = db.prepare("SELECT * FROM agents WHERE project_id = ? AND id = ?").get(projectId, id);
7
+ if (!row)
8
+ throw new Error(`No agent ${id} in project ${projectId}. ${notFoundHint}`);
9
+ return row;
10
+ }
11
+ function closedAgentNamed(projectId, needle) {
12
+ return db
13
+ .prepare(`SELECT id, name, kind, parked_at FROM agents WHERE project_id = ? AND status != 'running' ORDER BY ${CLOSED_ROW_ORDER}`)
14
+ .all(projectId).find((r) => r.name.toLowerCase() === needle);
15
+ }
16
+ export function findAgent(projectId, ref) {
17
+ if (ref.agent_id != null) {
18
+ return getAgentRow(projectId, ref.agent_id, "Call agent_list.");
19
+ }
20
+ if (ref.name) {
21
+ const rows = db
22
+ .prepare("SELECT * FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
23
+ .all(projectId);
24
+ const exact = rows.filter((r) => r.name === ref.name);
25
+ if (exact.length === 1)
26
+ return exact[0];
27
+ if (exact.length > 1) {
28
+ throw new Error(`Multiple running agents named "${ref.name}". Target by agent_id instead.`);
29
+ }
30
+ const needle = ref.name.toLowerCase();
31
+ const sameName = rows.filter((r) => r.name.toLowerCase() === needle);
32
+ if (sameName.length === 1)
33
+ return sameName[0];
34
+ const closed = closedAgentNamed(projectId, needle);
35
+ if (closed) {
36
+ if (closed.parked_at) {
37
+ throw new Error(`Agent ${closed.id} ("${closed.name}") is PARKED, not finished - it was paused on ` +
38
+ `${closed.parked_at} and its session is waiting. Bring it back with agent_resume(agent_id: ` +
39
+ `${closed.id}), or abandon the park with agent_close(agent_id: ${closed.id}).`);
40
+ }
41
+ const remedy = closed.kind === LEAD_KIND ? "Run `hive lead` to start a new one" : "Spawn a new worker";
42
+ throw new Error(`Agent ${closed.id} ("${closed.name}") is closed. ${remedy}, or target a running one by name or agent_id.`);
43
+ }
44
+ const partial = rows.filter((r) => r.name.toLowerCase().includes(needle));
45
+ if (partial.length === 1)
46
+ return partial[0];
47
+ if (partial.length > 1) {
48
+ const candidates = partial.map((r) => `${r.name} (agent_id ${r.id})`).join(", ");
49
+ throw new Error(`"${ref.name}" matches ${partial.length} running agents: ${candidates}. Use the full name or agent_id.`);
50
+ }
51
+ if (/^\d+$/.test(ref.name)) {
52
+ const id = Number(ref.name);
53
+ if (Number.isSafeInteger(id)) {
54
+ const byId = db
55
+ .prepare("SELECT * FROM agents WHERE project_id = ? AND id = ? AND status = 'running'")
56
+ .get(projectId, id);
57
+ if (byId)
58
+ return byId;
59
+ }
60
+ }
61
+ else if (/^[a-z]+:\d+$/i.test(ref.name)) {
62
+ const byActorId = db
63
+ .prepare("SELECT * FROM agents WHERE project_id = ? AND actor_id = ? AND status = 'running'")
64
+ .get(projectId, ref.name);
65
+ if (byActorId)
66
+ return byActorId;
67
+ }
68
+ throw new Error(`No running agent matching "${ref.name}" in project ${projectId}. Call agent_list.`);
69
+ }
70
+ throw new Error("Pass agent_id or name.");
71
+ }
72
+ export const PROBE_FAILED_NOTE = "tmux could not be probed, so liveness is unknown. Nothing was changed. Retry in a few seconds.";
73
+ const unknownOwnershipNote = (snapshot) => observationFailed(snapshot) ? PROBE_FAILED_NOTE : UNVERIFIED_OWNERSHIP_NOTE;
74
+ export const UNVERIFIED_OWNERSHIP_NOTE = "This row's pane cannot be verified as its own: it is on another tmux socket, or no pane pid was recorded for it, so liveness is unknown and no screen was read. Nothing was changed. If the row is stale, a human or peer lead can retire it with agent_close(row_only=true).";
75
+ export const probeFailed = (agent) => new Error(`Agent ${agent.id} ("${agent.name}"): ${PROBE_FAILED_NOTE}`);
76
+ // A failed probe throws rather than answering unknown, so callers keep the retry wording.
77
+ export function observeOwnership(agent) {
78
+ const ownership = observeRowOwnership(agent);
79
+ if (ownership === "probe-failed")
80
+ throw probeFailed(agent);
81
+ return ownership;
82
+ }
83
+ export const REISSUED_PANE_NOTE = "Its recorded pane id now belongs to a different process (the recorded pane pid no longer matches), so its own process is gone and the pane is someone else's. Nothing was typed, read or killed there.";
84
+ export function notOwnedError(agent, ownership) {
85
+ const who = `Agent ${agent.id} ("${agent.name}")`;
86
+ if (ownership === "gone") {
87
+ return new Error(`${who} has no live tmux window (its process exited or the window was killed). Close it with agent_close and spawn a new one.`);
88
+ }
89
+ if (ownership === "reissued") {
90
+ return new Error(`${who}: pane ownership reads reissued. ${REISSUED_PANE_NOTE} Close the row with agent_close, which retires it without touching that pane.`);
91
+ }
92
+ return new Error(`${who}: pane ownership reads unknown. ${UNVERIFIED_OWNERSHIP_NOTE}`);
93
+ }
94
+ export function summaryLiveness(row, snapshot) {
95
+ if (row.status !== "running")
96
+ return false;
97
+ return ownershipLiveness(rowOwnership(row, snapshot));
98
+ }
package/dist/backup.js CHANGED
@@ -2,7 +2,7 @@ import SqliteDb from "better-sqlite3";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { errorMessage } from "./result.js";
5
+ import { errorMessage } from "./errors.js";
6
6
  export const BACKUP_DIRNAME = "backups";
7
7
  export function backupsDir(dataDir) {
8
8
  return join(dataDir, BACKUP_DIRNAME);
@@ -1 +1 @@
1
- {"version":"1.11.5","sha":"486161d","dirty":false,"build_id":"3a33da919c1ac52f423bfdb530cfeda01207921e9c0fb8b262462fae1c266b12"}
1
+ {"version":"1.11.7","sha":"4861c81","dirty":false,"build_id":"2b159fa2c2519fbd7edef9cf4396ac349c3fb25ec433f76a9b91c5ad98188f43"}
@@ -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,7 +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 { ACTIVE_TIMER_WHERE, socketAwaitingWhere, dashboardFileContained, describeStall, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, } from "./scheduler.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";
27
28
  import { readTurnCount } from "./turnCount.js";
28
29
  import { leadSessionEnded } from "./leadState.js";
29
30
  import { heldReasonLabel } from "./heldLabel.js";
@@ -47,9 +48,9 @@ import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
47
48
  import { ageSecondsSince, deriveProvenance, describeForHuman, describeLastLogEvent, humanizeAge, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "./stateProvenance.js";
48
49
  import { awaitingFirstPromptSql } from "./firstPrompt.js";
49
50
  import { checkoutRoot, createProfile, forkProfile, PROFILE_FILES, ProfileError, profileExists, profileFileNames, profileNames, profileStatus, readProfileFile, referencedPads, referencedPaths, renderProfileFile, resolveProfileFile, REWRITE_THRESHOLD, templateVars, userProfilesDir, } from "./profiles.js";
50
- import { getTodoDetail, listTodoSummaries, TODO_STATUSES, } from "./tools/todos.js";
51
- import { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
52
- import { createPad, getActivePadByName, listActivePads, overwritePadContent, } from "./tools/pads.js";
51
+ import { getTodoDetail, listTodoSummaries, } from "./todos.js";
52
+ import { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL, TODO_STATUSES } from "./todoScope.js";
53
+ import { createPad, getActivePadByName, listActivePads, overwritePadContent, } from "./pads.js";
53
54
  import { answerQuestion, listQuestions } from "./questions.js";
54
55
  function usage() {
55
56
  console.log(`hive — run a crew of Claude Code and Codex workers from one lead session
@@ -389,14 +390,11 @@ function reportMigrationResult(result) {
389
390
  console.error(`! ${warning}`);
390
391
  }
391
392
  async function cmdLead(argv) {
392
- const unknownFlag = argv.find((a) => a.startsWith("--") && a !== "--no-dashboard" && a !== "--detach");
393
- if (unknownFlag !== undefined) {
394
- console.error(`hive lead: unknown flag "${unknownFlag}". Flags are --no-dashboard and --detach.`);
395
- process.exit(1);
396
- }
397
- const noDashboard = argv.includes("--no-dashboard");
398
- const detach = argv.includes("--detach");
399
- 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];
400
398
  let registrationNotice = null;
401
399
  try {
402
400
  const project = resolveProjectForWrite("hive lead", path, (text) => {
@@ -1162,6 +1160,8 @@ function profileSizeAdvisory(name, file) {
1162
1160
  }
1163
1161
  function cmdProfile(argv) {
1164
1162
  const [sub, ...rest] = argv;
1163
+ if (sub?.startsWith("-"))
1164
+ rejectUnknownFlags("profile", parseArgs(argv, {}), "none");
1165
1165
  try {
1166
1166
  switch (sub) {
1167
1167
  case undefined:
@@ -1339,12 +1339,9 @@ function openDashboardOnce(project, dashboardFile) {
1339
1339
  }
1340
1340
  }
1341
1341
  function cmdAttach(argv) {
1342
- const unknownFlag = argv.find((a) => a.startsWith("--"));
1343
- if (unknownFlag !== undefined) {
1344
- console.error(`hive attach: unknown flag "${unknownFlag}". hive attach takes no flags.`);
1345
- process.exit(1);
1346
- }
1347
- const path = argv.find((a) => !a.startsWith("--"));
1342
+ const parsed = parseArgs(argv, {});
1343
+ rejectUnknownFlags("attach", parsed, "none");
1344
+ const path = parsed.positional[0];
1348
1345
  const project = resolveProjectForWrite("hive attach", path);
1349
1346
  maybeOpenDashboard(project, !!loadProjectYml(project.path).config?.dashboard);
1350
1347
  const session = sessionName();
@@ -1495,8 +1492,7 @@ const leadPaneExitedHookArmed = () => globalHooks(LEAD_PANE_EXITED_HOOK).some((e
1495
1492
  function cmdLeadPaneExited(argv) {
1496
1493
  try {
1497
1494
  const parsed = parseArgs(argv, {});
1498
- if (parsed.unknown.length > 0)
1499
- return;
1495
+ rejectUnknownFlags("lead-pane-exited", parsed, "none");
1500
1496
  const pane = parsed.positional[0];
1501
1497
  if (!pane)
1502
1498
  return;
@@ -2314,14 +2310,13 @@ function reportEffectiveConfig(loaded) {
2314
2310
  : `${autoAttach} (default; set with \`hive setup --auto-attach\`; source: built-in)`);
2315
2311
  }
2316
2312
  function cmdDoctor(argv) {
2317
- const strict = argv.includes("--strict");
2318
- doctorVerbose = argv.includes("--verbose");
2319
- const KNOWN_DOCTOR_FLAGS = new Set(["--strict", "--verbose"]);
2320
- const unknown = argv.find((a) => !KNOWN_DOCTOR_FLAGS.has(a));
2321
- if (unknown !== undefined) {
2322
- console.error(`hive doctor: unknown argument "${unknown}". Flags are --strict and --verbose.`);
2323
- process.exit(1);
2324
- }
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");
2325
2320
  reportMigrationResult(migrateLegacyConfig());
2326
2321
  let failures = 0;
2327
2322
  const fail = (label, ...lines) => {
@@ -2800,13 +2795,13 @@ function cmdStatusline() {
2800
2795
  const held = db
2801
2796
  .prepare(`WITH chosen AS (
2802
2797
  SELECT held_reason, first_held_at FROM wakes
2803
- WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND held_at IS NOT NULL
2798
+ WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND ${heldWakeWhere()}
2804
2799
  ORDER BY (held_reason LIKE ?) DESC, (held_reason LIKE ?) ASC,
2805
2800
  first_held_at IS NULL ASC, first_held_at ASC
2806
2801
  LIMIT 1
2807
2802
  )
2808
2803
  SELECT
2809
- (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,
2810
2805
  (SELECT held_reason FROM chosen) AS held_reason,
2811
2806
  (SELECT first_held_at FROM chosen) AS first_held_at`)
2812
2807
  .get(project.id, `${HELD_REASON_UNSUBMITTED_INPUT_PREFIX}%`, `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}%`, project.id);
@@ -3255,11 +3250,26 @@ function requireFlagValues(command, parsed) {
3255
3250
  process.exit(1);
3256
3251
  }
3257
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);
3258
3269
  const project = resolveProjectForRead("todos");
3259
- const all = argv.includes("--all");
3260
- const statusIdx = argv.indexOf("--status");
3261
- const status = flagValue(argv, "--status");
3262
- 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) {
3263
3273
  console.log(`Usage: hive todos --status <s> where <s> is one of: ${TODO_STATUSES.join(", ")}`);
3264
3274
  process.exit(1);
3265
3275
  }
@@ -3267,9 +3277,8 @@ function cmdTodos(argv) {
3267
3277
  console.log(`Unknown status "${status}". Valid: ${TODO_STATUSES.join(", ")}`);
3268
3278
  process.exit(1);
3269
3279
  }
3270
- const tagIdx = argv.indexOf("--tag");
3271
- const tag = flagValue(argv, "--tag");
3272
- if (tagIdx !== -1 && (tag === undefined || tag.startsWith("--"))) {
3280
+ const tag = parsed.values.get("--tag");
3281
+ if (parsed.values.has("--tag") && tag === undefined) {
3273
3282
  console.log("Usage: hive todos --tag <t>");
3274
3283
  process.exit(1);
3275
3284
  }
@@ -3299,8 +3308,10 @@ function cmdTodos(argv) {
3299
3308
  }
3300
3309
  }
3301
3310
  function cmdTodo(argv) {
3311
+ const parsed = parseArgs(argv, {});
3312
+ rejectUnknownFlags("todo", parsed, "none");
3302
3313
  const project = resolveProjectForRead("todo");
3303
- const id = Number(argv.find((a) => !a.startsWith("--")));
3314
+ const id = Number(parsed.positional[0]);
3304
3315
  if (!Number.isInteger(id)) {
3305
3316
  console.log("Usage: hive todo <id> (run inside the project)");
3306
3317
  process.exit(1);
@@ -3504,6 +3515,8 @@ let rest = args.slice(1);
3504
3515
  if (command === "--help" || command === "-h" || command === "help")
3505
3516
  usage();
3506
3517
  if (command === "--version" || command === "-v") {
3518
+ const parsed = parseArgs(rest, { flags: ["--check"] });
3519
+ rejectUnknownFlags("version", parsed, "--check");
3507
3520
  const { line, drift } = versionInfo();
3508
3521
  console.log(line);
3509
3522
  if (drift)
@@ -3583,6 +3596,7 @@ try {
3583
3596
  cmdHide(rest);
3584
3597
  break;
3585
3598
  case "status":
3599
+ rejectUnknownFlags("status", parseArgs(rest, {}), "none");
3586
3600
  cmdStatus();
3587
3601
  break;
3588
3602
  case "portfolio":
@@ -3607,6 +3621,7 @@ try {
3607
3621
  cmdDoctor(rest);
3608
3622
  break;
3609
3623
  case "pads":
3624
+ rejectUnknownFlags("pads", parseArgs(rest, {}), "none");
3610
3625
  cmdPads();
3611
3626
  break;
3612
3627
  case "pad":
@@ -3625,6 +3640,7 @@ try {
3625
3640
  cmdAnswer(rest);
3626
3641
  break;
3627
3642
  case "backups":
3643
+ rejectUnknownFlags("backups", parseArgs(rest, {}), "none");
3628
3644
  cmdBackups();
3629
3645
  break;
3630
3646
  case "restore":
@@ -3633,19 +3649,29 @@ try {
3633
3649
  case "project":
3634
3650
  await cmdProject(rest);
3635
3651
  break;
3636
- case "runbook":
3637
- cmdRunbook(rest[0]);
3652
+ case "runbook": {
3653
+ const parsed = parseArgs(rest, {});
3654
+ rejectUnknownFlags("runbook", parsed, "none");
3655
+ cmdRunbook(parsed.positional[0]);
3638
3656
  break;
3639
- case "posture":
3640
- cmdPosture(rest[0]);
3657
+ }
3658
+ case "posture": {
3659
+ const parsed = parseArgs(rest, {});
3660
+ rejectUnknownFlags("posture", parsed, "none");
3661
+ cmdPosture(parsed.positional[0]);
3641
3662
  break;
3663
+ }
3642
3664
  case "profile":
3643
3665
  cmdProfile(rest);
3644
3666
  break;
3645
- case "kickoff":
3667
+ case "kickoff": {
3668
+ const parsed = parseArgs(rest, { flags: ["--explain", "--codex"] });
3669
+ rejectUnknownFlags("kickoff", parsed, "--explain, --codex");
3646
3670
  await (await import("./kickoff.js")).runKickoff(rest);
3647
3671
  break;
3672
+ }
3648
3673
  case "statusline":
3674
+ rejectUnknownFlags("statusline", parseArgs(rest, {}), "none");
3649
3675
  cmdStatusline();
3650
3676
  break;
3651
3677
  default:
package/dist/context.js CHANGED
@@ -3,9 +3,9 @@ import { userInfo } from "node:os";
3
3
  import { basename, dirname, join, resolve, sep } from "node:path";
4
4
  import { storeDir } from "./dataDir.js";
5
5
  import { db } from "./db.js";
6
- import { LEAD_KIND } from "./spawn.js";
6
+ import { LEAD_KIND } from "./agentKinds.js";
7
7
  import { rowOwnership } from "./tmux.js";
8
- import { recordQueenRefusal } from "./queenAudit.js";
8
+ import { recordQueenRefusal } from "./queenAuditStore.js";
9
9
  import { gitOut } from "./git.js";
10
10
  let selectedId = null;
11
11
  let cachedActorId = null;
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";
@@ -8,7 +9,7 @@ import { ACTIVE_TODO_SQL } from "./todoScope.js";
8
9
  import { pendingQuestionCount } from "./questions.js";
9
10
  import { loadProjectYml } from "./projectYml.js";
10
11
  import { parseTags } from "./result.js";
11
- import { ACTIVE_TIMER_WHERE } from "./scheduler.js";
12
+ import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
12
13
  import { reportsAgentStateLog } from "./stateProvenance.js";
13
14
  import { gitOut } from "./git.js";
14
15
  import { liveTargets, rowOwnership } from "./tmux.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
@@ -1,8 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { createRequire } from "node:module";
2
+ import { listQuestions, pendingQuestionCount } from "./questions.js";
3
+ import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
3
4
  import { db } from "./db.js";
4
5
  import { ACTIVE_TODO_SQL, NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
5
6
  import { getProject } from "./context.js";
7
+ import { parseStoreTimestamp } from "./storeClock.js";
6
8
  export const describeVisibility = (visibility) => visibility === null ? "running" : visibility === "window" ? "own window" : visibility;
7
9
  export function processCounts(procs) {
8
10
  return {
@@ -12,9 +14,8 @@ export function processCounts(procs) {
12
14
  notStarted: procs.filter((p) => !p.running).length,
13
15
  };
14
16
  }
15
- import { awaitingFirstPrompt } from "./firstPrompt.js";
17
+ import { finishedIdle } from "./firstPrompt.js";
16
18
  import { SLUG_MAX_LEN, cutToUnitBudget } from "./slug.js";
17
- const requireFromHere = createRequire(import.meta.url);
18
19
  export const TODO_CAP = 100;
19
20
  export const AGENT_CAP = 50;
20
21
  export const WAKE_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);
@@ -87,7 +88,6 @@ function fetchProject(projectId) {
87
88
  throw new Error(`No project ${projectId}.`);
88
89
  return row;
89
90
  }
90
- const PENDING_WAKE_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
91
91
  function agentStatusLevel(state) {
92
92
  if (state === "working")
93
93
  return "live";
@@ -98,7 +98,7 @@ function agentStatusLevel(state) {
98
98
  return "warn";
99
99
  }
100
100
  function agentStateBadge(a) {
101
- return awaitingFirstPrompt(a) && a.agent_state === "idle"
101
+ return a.agent_state === "idle" && !finishedIdle(a)
102
102
  ? statusBadge("live", "idle (no assignment yet)")
103
103
  : statusBadge(agentStatusLevel(a.agent_state), a.agent_state);
104
104
  }
@@ -121,7 +121,7 @@ function fetchNowAgents(projectId) {
121
121
  function fetchNextWake(projectId) {
122
122
  return db
123
123
  .prepare(`SELECT kind, due_at, max_wait_at, deliver_actor FROM wakes
124
- WHERE project_id = ? AND ${PENDING_WAKE_WHERE}
124
+ WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}
125
125
  ORDER BY COALESCE(due_at, max_wait_at, created_at) LIMIT 1`)
126
126
  .get(projectId);
127
127
  }
@@ -428,7 +428,7 @@ function renderThroughputSection(index, stats) {
428
428
  function renderWakesSection(index, projectId) {
429
429
  const all = db
430
430
  .prepare(`SELECT id, kind, body, owner, deliver_actor, due_at, max_wait_at, fire_count, repeat_every_ms, held_at
431
- FROM wakes WHERE project_id = ? AND ${PENDING_WAKE_WHERE}
431
+ FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}
432
432
  ORDER BY COALESCE(due_at, max_wait_at, created_at)`)
433
433
  .all(projectId);
434
434
  const shown = all.slice(0, WAKE_CAP);
@@ -449,7 +449,7 @@ function renderWakesSection(index, projectId) {
449
449
  return section(index, "wakes", "Wakes", `${all.length} pending`, "bell", String(shown.length), body);
450
450
  }
451
451
  function questionAge(createdAt) {
452
- const timestamp = Date.parse(`${createdAt.replace(" ", "T")}Z`);
452
+ const timestamp = parseStoreTimestamp(createdAt);
453
453
  if (!Number.isFinite(timestamp))
454
454
  return "unknown";
455
455
  const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
@@ -462,8 +462,6 @@ function questionAge(createdAt) {
462
462
  return "now";
463
463
  }
464
464
  function renderQuestionsSection(index, projectId) {
465
- // A static import of questions.js cycles through tools/wakes.ts and scheduler.ts.
466
- const { listQuestions, pendingQuestionCount } = requireFromHere("./questions.js");
467
465
  const questions = listQuestions(projectId);
468
466
  if (questions.length === 0)
469
467
  return "";
package/dist/db.js CHANGED
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { guardAbi } from "./abi.js";
5
5
  import { guardStoreDir } from "./dataDir.js";
6
6
  import { maybeBackupBeforeMigrations } from "./backup.js";
7
- import { errorMessage } from "./result.js";
7
+ import { errorMessage } from "./errors.js";
8
8
  export const dataDir = guardStoreDir();
9
9
  mkdirSync(dataDir, { recursive: true });
10
10
  function sleepSync(ms) {
package/dist/errors.js ADDED
@@ -0,0 +1,3 @@
1
+ export function errorMessage(e) {
2
+ return e instanceof Error ? e.message : String(e);
3
+ }
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
+ }
@@ -3,7 +3,7 @@ import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, readFileSync, ren
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { isMap, parse, parseDocument } from "yaml";
5
5
  import { storeDir } from "./dataDir.js";
6
- import { errorMessage } from "./result.js";
6
+ import { errorMessage } from "./errors.js";
7
7
  export const ATTACH_MODES = ["auto", "raw", "control"];
8
8
  export const AUTO_ATTACH_MODES = ["auto", "on", "off"];
9
9
  export function isAttachMode(value) {
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"];