@cmgmyr/hive 1.11.5 → 1.11.6

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.
@@ -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.6","sha":"b4e9fb0","dirty":false,"build_id":"fb0debbc0e5e030650660c10505b25c26e60160403f384ce900920146e639961"}
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 { 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";
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
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
@@ -8,7 +8,7 @@ import { ACTIVE_TODO_SQL } from "./todoScope.js";
8
8
  import { pendingQuestionCount } from "./questions.js";
9
9
  import { loadProjectYml } from "./projectYml.js";
10
10
  import { parseTags } from "./result.js";
11
- import { ACTIVE_TIMER_WHERE } from "./scheduler.js";
11
+ import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
12
12
  import { reportsAgentStateLog } from "./stateProvenance.js";
13
13
  import { gitOut } from "./git.js";
14
14
  import { liveTargets, rowOwnership } from "./tmux.js";
package/dist/dashboard.js CHANGED
@@ -1,5 +1,6 @@
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";
@@ -14,7 +15,6 @@ export function processCounts(procs) {
14
15
  }
15
16
  import { awaitingFirstPrompt } from "./firstPrompt.js";
16
17
  import { SLUG_MAX_LEN, cutToUnitBudget } from "./slug.js";
17
- const requireFromHere = createRequire(import.meta.url);
18
18
  export const TODO_CAP = 100;
19
19
  export const AGENT_CAP = 50;
20
20
  export const WAKE_CAP = 50;
@@ -87,7 +87,6 @@ function fetchProject(projectId) {
87
87
  throw new Error(`No project ${projectId}.`);
88
88
  return row;
89
89
  }
90
- const PENDING_WAKE_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
91
90
  function agentStatusLevel(state) {
92
91
  if (state === "working")
93
92
  return "live";
@@ -121,7 +120,7 @@ function fetchNowAgents(projectId) {
121
120
  function fetchNextWake(projectId) {
122
121
  return db
123
122
  .prepare(`SELECT kind, due_at, max_wait_at, deliver_actor FROM wakes
124
- WHERE project_id = ? AND ${PENDING_WAKE_WHERE}
123
+ WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}
125
124
  ORDER BY COALESCE(due_at, max_wait_at, created_at) LIMIT 1`)
126
125
  .get(projectId);
127
126
  }
@@ -428,7 +427,7 @@ function renderThroughputSection(index, stats) {
428
427
  function renderWakesSection(index, projectId) {
429
428
  const all = db
430
429
  .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}
430
+ FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}
432
431
  ORDER BY COALESCE(due_at, max_wait_at, created_at)`)
433
432
  .all(projectId);
434
433
  const shown = all.slice(0, WAKE_CAP);
@@ -462,8 +461,6 @@ function questionAge(createdAt) {
462
461
  return "now";
463
462
  }
464
463
  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
464
  const questions = listQuestions(projectId);
468
465
  if (questions.length === 0)
469
466
  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
+ }
@@ -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/kickoff.js CHANGED
@@ -17,9 +17,9 @@ export function truncate(text, limit) {
17
17
  async function digest(projectPath, profile, warnings) {
18
18
  const { migrate, db } = await import("./db.js");
19
19
  const { findProjectForCwd } = await import("./context.js");
20
- const { ACTIVE_TIMER_WHERE } = await import("./scheduler.js");
21
- const { getActivePadByName } = await import("./tools/pads.js");
22
20
  const { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } = await import("./todoScope.js");
21
+ const { ACTIVE_TIMER_WHERE } = await import("./wakeSql.js");
22
+ const { getActivePadByName } = await import("./pads.js");
23
23
  migrate();
24
24
  const project = findProjectForCwd();
25
25
  if (!project || project.path !== projectPath)
@@ -4,7 +4,7 @@ import { carriesNameFlag } from "./harnesses.js";
4
4
  import { join } from "node:path";
5
5
  import { checkoutRoot, renderProfileFile, resolveProfileFile } from "./profiles.js";
6
6
  import { activeProfile, configHash, mergedProjectVars } from "./projectYml.js";
7
- import { LEAD_NAME } from "./spawn.js";
7
+ import { LEAD_NAME } from "./agentKinds.js";
8
8
  import { shellQuote } from "./tmux.js";
9
9
  // What `hive lead` and the handoff bootstrap both need to launch a claude lead: trust, posture,
10
10
  // hooks and identity env. Pane adoption and leftover cleanup stay in cmdLead.
package/dist/leadWatch.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { db } from "./db.js";
2
2
  import { paneClassifierFor } from "./harnesses.js";
3
3
  import { readLeadTurnState } from "./leadState.js";
4
- import { LEAD_KIND } from "./spawn.js";
4
+ import { LEAD_KIND } from "./agentKinds.js";
5
5
  import { foreignSocket, holdsHumanInput, observationFailed, rowOwnership } from "./tmux.js";
6
6
  export const LEAD_TARGET_GONE = "LEAD_TARGET_GONE";
7
7
  export const LEAD_TARGET_REISSUED = "LEAD_TARGET_REISSUED";
package/dist/pads.js ADDED
@@ -0,0 +1,70 @@
1
+ import { db } from "./db.js";
2
+ import { currentActor } from "./context.js";
3
+ export function splitsSurrogatePair(content, index) {
4
+ if (index <= 0 || index >= content.length)
5
+ return false;
6
+ const before = content.charCodeAt(index - 1);
7
+ const after = content.charCodeAt(index);
8
+ return before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
9
+ }
10
+ function selectPad(projectId, padId, columns) {
11
+ const row = db.prepare(`SELECT ${columns} FROM pads WHERE project_id = ? AND id = ?`).get(projectId, padId);
12
+ if (!row)
13
+ throw new Error(`No pad with id ${padId} in project ${projectId}. Call pad_list.`);
14
+ return row;
15
+ }
16
+ export function getPad(projectId, padId) {
17
+ return selectPad(projectId, padId, "*");
18
+ }
19
+ export function getPadMeta(projectId, padId) {
20
+ return selectPad(projectId, padId, "id, name, revision, archived");
21
+ }
22
+ function revisionMismatchError(padId, expected, current) {
23
+ if (current == null) {
24
+ return new Error(`Pad ${padId} no longer exists; it may have just been deleted by another write. Call pad_list to confirm.`);
25
+ }
26
+ return new Error(`Revision mismatch for pad ${padId}: expected ${expected}, current ${current}. Re-read with pad_read and retry.`);
27
+ }
28
+ function currentRevisionOrDeleted(padId) {
29
+ const row = db.prepare("SELECT revision FROM pads WHERE id = ?").get(padId);
30
+ return row?.revision ?? null;
31
+ }
32
+ export function assertRowChanged(padId, predicateRevision, changed) {
33
+ if (!changed)
34
+ throw revisionMismatchError(padId, predicateRevision, currentRevisionOrDeleted(padId));
35
+ }
36
+ export function checkRevision(pad, expected, required) {
37
+ if (expected == null) {
38
+ if (required)
39
+ throw new Error(`expected_revision is required to overwrite pad ${pad.id}. Current revision: ${pad.revision}. Read it first with pad_read.`);
40
+ return;
41
+ }
42
+ if (expected !== pad.revision)
43
+ throw revisionMismatchError(pad.id, expected, pad.revision);
44
+ }
45
+ export function bumpPad(padId, predicateRevision, set, ...params) {
46
+ const where = predicateRevision != null ? "id = ? AND revision = ?" : "id = ?";
47
+ const whereParams = predicateRevision != null ? [padId, predicateRevision] : [padId];
48
+ const row = db.prepare(`UPDATE pads SET ${set}, revision = revision + 1,
49
+ updated_by = ?, updated_at = datetime('now') WHERE ${where} RETURNING revision`).get(...params, currentActor(), ...whereParams);
50
+ assertRowChanged(padId, predicateRevision, row != null);
51
+ return row.revision;
52
+ }
53
+ export const APPEND_WITH_SEPARATOR_SET = "content = content || (CASE WHEN content = '' OR substr(content, -1) = char(10) THEN '' ELSE char(10) END) || ?";
54
+ export function getActivePadByName(projectId, name) {
55
+ return db.prepare("SELECT * FROM pads WHERE project_id = ? AND name = ? AND archived = 0").get(projectId, name);
56
+ }
57
+ export function listActivePads(projectId) {
58
+ return db.prepare(`SELECT name, revision, length(content) AS content_length, updated_by, updated_at
59
+ FROM pads WHERE project_id = ? AND archived = 0 ORDER BY name`).all(projectId);
60
+ }
61
+ export function overwritePadContent(projectId, padId, content, expectedRevision) {
62
+ const pad = getPadMeta(projectId, padId);
63
+ checkRevision(pad, expectedRevision, true);
64
+ return bumpPad(padId, pad.revision, "content = ?", content);
65
+ }
66
+ export function createPad(projectId, name, content, tags) {
67
+ const row = db.prepare(`INSERT OR IGNORE INTO pads (project_id, name, content, tags, updated_by)
68
+ VALUES (?, ?, ?, ?, ?) RETURNING id`).get(projectId, name, content, JSON.stringify(tags), currentActor());
69
+ return row?.id ?? null;
70
+ }
package/dist/portfolio.js CHANGED
@@ -4,6 +4,8 @@ import { existsSync } from "node:fs";
4
4
  import { parseTags } from "./result.js";
5
5
  import { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
6
6
  import { pendingQuestionCount } from "./questions.js";
7
+ import { ACTIVE_TIMER_WHERE as ACTIVE_WAKE_WHERE } from "./wakeSql.js";
8
+ export { ACTIVE_WAKE_WHERE };
7
9
  import { leadSessionEnded, readLeadTurnState } from "./leadState.js";
8
10
  import { liveTargets, ownershipLiveness, rowOwnership } from "./tmux.js";
9
11
  export const PORTFOLIO_REASONS = [
@@ -26,7 +28,6 @@ const NEEDS_HUMAN_ITEM_CAP = 5;
26
28
  const WAKE_OVERDUE_GRACE = "-5 minutes";
27
29
  const STALE_IN_PROGRESS = "-48 hours";
28
30
  const WAKE_UPCOMING = "+24 hours";
29
- export const ACTIVE_WAKE_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
30
31
  export function leadText(p) {
31
32
  return p.lead.state === "alive" && p.lead.turn !== "unknown"
32
33
  ? `alive, turn ${p.lead.turn === "working" ? "working" : "ended"}`
@@ -1,6 +1,6 @@
1
1
  import { backupNow } from "./backup.js";
2
2
  import { dataDir, db } from "./db.js";
3
- import { LEAD_KIND } from "./spawn.js";
3
+ import { LEAD_KIND } from "./agentKinds.js";
4
4
  import { foreignSocket, liveTargets, observationFailed, rowOwnership } from "./tmux.js";
5
5
  export const PROJECT_OWNER_TABLES = [
6
6
  "pads",
@@ -4,7 +4,7 @@ import { resolve, sep } from "node:path";
4
4
  import { parse } from "yaml";
5
5
  import { harnessNames } from "./harnesses.js";
6
6
  import { isValidProfileName } from "./profiles.js";
7
- import { errorMessage } from "./result.js";
7
+ import { errorMessage } from "./errors.js";
8
8
  import { isWindowLayout, WINDOW_LAYOUTS } from "./tmux.js";
9
9
  import { globalConfigPath, isEmptyYamlDocument, readGlobalConfig, resolveAttachSetting, } from "./globalConfig.js";
10
10
  export const BUILT_IN_PROJECT_YML = Object.freeze({
@@ -1,11 +1,8 @@
1
1
  import { captureQueenWriteIdentity, effectiveProjectId, isQueenLead } from "./context.js";
2
+ import { insertQueenAuditRow, QUEEN_AUDIT_DEFAULT_LIMIT, QUEEN_AUDIT_MAX_LIMIT, QUEEN_AUDIT_SUMMARY_MAX, } from "./queenAuditStore.js";
2
3
  import { db } from "./db.js";
3
4
  import { cutToUnitBudget, flatten } from "./slug.js";
4
- export const QUEEN_AUDIT_SUMMARY_MAX = 160;
5
- export const QUEEN_AUDIT_DEFAULT_LIMIT = 20;
6
- export const QUEEN_AUDIT_MAX_LIMIT = 100;
7
- export const QUEEN_AUDIT_RETENTION = "-30 days";
8
- export const QUEEN_AUDIT_MAX_ROWS = 20_000;
5
+ export { QUEEN_AUDIT_DEFAULT_LIMIT, QUEEN_AUDIT_MAX_LIMIT, QUEEN_AUDIT_MAX_ROWS, QUEEN_AUDIT_RETENTION, QUEEN_AUDIT_SUMMARY_MAX, recordQueenRefusal, } from "./queenAuditStore.js";
9
6
  function resource(type, value) {
10
7
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? { type, id: value } : null;
11
8
  }
@@ -13,11 +10,6 @@ function removalSummary(r) {
13
10
  const counts = Object.entries((r.counts ?? {})).map(([t, n]) => `${t} ${n}`);
14
11
  return `removed project "${r.name}"${counts.length > 0 ? ` with ${counts.join(", ")}` : ""}`;
15
12
  }
16
- function insertQueenAuditRow(operation, targetProjectId, identity, resourceType, resourceId, summary, outcome = "confirmed", problem = "") {
17
- db.prepare(`INSERT INTO queen_audit
18
- (actor_id, home_project_id, target_project_id, operation, resource_type, resource_id, summary, outcome, problem)
19
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(identity.actor_id, identity.home_project_id, targetProjectId, operation, resourceType, resourceId, summary, outcome, problem);
20
- }
21
13
  export const QUEEN_AUDIT_OPERATIONS = {
22
14
  todo_create: {
23
15
  mode: "database",
@@ -85,14 +77,6 @@ function insertAudit(operation, targetProjectId, args, receipt, identity) {
85
77
  const summary = text.length <= QUEEN_AUDIT_SUMMARY_MAX ? text : cutToUnitBudget(text, QUEEN_AUDIT_SUMMARY_MAX - 1) + "…";
86
78
  insertQueenAuditRow(operation, targetProjectId, identity, touched.type, touched.id, summary);
87
79
  }
88
- export function recordQueenRefusal(operation, targetProjectId, problem, identity) {
89
- const text = flatten(problem);
90
- const summaryText = `refused: ${text}`;
91
- const summary = summaryText.length <= QUEEN_AUDIT_SUMMARY_MAX
92
- ? summaryText
93
- : cutToUnitBudget(summaryText, QUEEN_AUDIT_SUMMARY_MAX - 1) + "…";
94
- insertQueenAuditRow(operation, targetProjectId, identity, "project", targetProjectId, summary, "refused", text);
95
- }
96
80
  export function commitQueenStateWrite(operation, targetProjectId, args, mutate) {
97
81
  const identity = captureQueenWriteIdentity(targetProjectId);
98
82
  if (!identity)
@@ -0,0 +1,20 @@
1
+ import { db } from "./db.js";
2
+ import { cutToUnitBudget, flatten } from "./slug.js";
3
+ export const QUEEN_AUDIT_SUMMARY_MAX = 160;
4
+ export const QUEEN_AUDIT_DEFAULT_LIMIT = 20;
5
+ export const QUEEN_AUDIT_MAX_LIMIT = 100;
6
+ export const QUEEN_AUDIT_RETENTION = "-30 days";
7
+ export const QUEEN_AUDIT_MAX_ROWS = 20_000;
8
+ export function insertQueenAuditRow(operation, targetProjectId, identity, resourceType, resourceId, summary, outcome = "confirmed", problem = "") {
9
+ db.prepare(`INSERT INTO queen_audit
10
+ (actor_id, home_project_id, target_project_id, operation, resource_type, resource_id, summary, outcome, problem)
11
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(identity.actor_id, identity.home_project_id, targetProjectId, operation, resourceType, resourceId, summary, outcome, problem);
12
+ }
13
+ export function recordQueenRefusal(operation, targetProjectId, problem, identity) {
14
+ const text = flatten(problem);
15
+ const summaryText = `refused: ${text}`;
16
+ const summary = summaryText.length <= QUEEN_AUDIT_SUMMARY_MAX
17
+ ? summaryText
18
+ : cutToUnitBudget(summaryText, QUEEN_AUDIT_SUMMARY_MAX - 1) + "…";
19
+ insertQueenAuditRow(operation, targetProjectId, identity, "project", targetProjectId, summary, "refused", text);
20
+ }
package/dist/questions.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { db } from "./db.js";
2
2
  import { HUMAN_ACTOR_PREFIX } from "./context.js";
3
- import { assertSafeWakeBody, resolveWakeDelivery } from "./tools/wakes.js";
3
+ import { assertSafeWakeBody, resolveWakeDelivery } from "./wakeTarget.js";
4
4
  const QUESTION_STATE_CONFLICT = "QUESTION_STATE_CONFLICT";
5
5
  const HEADER_MAX_CHARS = 12;
6
6
  const MIN_OPTIONS = 2;
package/dist/result.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { runningBuildChange, runningBuildNotice } from "./version.js";
2
- export function errorMessage(e) {
3
- return e instanceof Error ? e.message : String(e);
4
- }
2
+ import { errorMessage } from "./errors.js";
3
+ export { errorMessage } from "./errors.js";
5
4
  function ok(data) {
6
5
  const text = typeof data === "string" ? data : JSON.stringify(data, null, 1);
7
6
  return { content: [{ type: "text", text }] };
package/dist/scheduler.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { QUEEN_AUDIT_RETENTION, QUEEN_AUDIT_MAX_ROWS, recentQueenAudit } from "./queenAudit.js";
2
+ import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
3
+ export { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
2
4
  import { runningBuildChange, runningBuildNotice, versionInfo } from "./version.js";
3
5
  import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
6
  import { createHash } from "node:crypto";
@@ -13,7 +15,8 @@ import { getProjectByPath, listProjects, queenHomeDir } from "./context.js";
13
15
  import { collectPortfolio } from "./portfolio.js";
14
16
  import { QUEEN_GENERATED_MARKER, readQueenBrief, renderQueenDashboard } from "./queenDashboard.js";
15
17
  import { COMMAND_KIND, stoppingMarkerLive } from "./processes.js";
16
- import { closeAgentRow, isLeadActorId, LEAD_ACTOR_PREFIX, LEAD_KIND, PaneOwnershipLost, reapCodexHomeForClosedAgent, requireStillOwned, } from "./spawn.js";
18
+ import { closeAgentRow, PaneOwnershipLost, reapCodexHomeForClosedAgent, requireStillOwned, } from "./spawn.js";
19
+ import { isLeadActorId, LEAD_ACTOR_PREFIX, LEAD_KIND, } from "./agentKinds.js";
17
20
  import { awaitingFirstPrompt, awaitingFirstPromptSql } from "./firstPrompt.js";
18
21
  import { evaluateLeadWatch, leadSubscription } from "./leadWatch.js";
19
22
  import { describeLiveTasks, describeOneTask, liveBackgroundTasks, STALL_BOUND_SECONDS, } from "./backgroundTasks.js";
@@ -56,7 +59,6 @@ function deliveryOwnership(row, snapshot) {
56
59
  return "closed";
57
60
  return rowOwnership(deliveryIdentity(row), snapshot);
58
61
  }
59
- export const ACTIVE_TIMER_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
60
62
  const SETTLE_WINDOW = "-15 seconds";
61
63
  const prepared = new Map();
62
64
  function stmt(sql) {
package/dist/spawn.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { preservedRolloutPath, reapCodexHome } from "./codexHome.js";
3
3
  import { getProject } from "./context.js";
4
+ import { LEAD_KIND, LEAD_NAME } from "./agentKinds.js";
4
5
  import { gitOut } from "./git.js";
5
6
  import { dataDir, db } from "./db.js";
6
7
  import { loadProjectYml } from "./projectYml.js";
@@ -113,13 +114,8 @@ export function asNameClash(e, name) {
113
114
  }
114
115
  return e;
115
116
  }
116
- export const LEAD_KIND = "lead";
117
- export const LEAD_ACTOR_PREFIX = `${LEAD_KIND}:`;
118
- export const isLeadActorId = (actorId) => actorId.startsWith(LEAD_ACTOR_PREFIX);
119
- export const mintLeadActorId = (agentId) => `${LEAD_ACTOR_PREFIX}${agentId}`;
117
+ export { LEAD_ACTOR_PREFIX, LEAD_KIND, LEAD_NAME, isLeadActorId, isReservedAgentName, mintLeadActorId } from "./agentKinds.js";
120
118
  export const isRunningLeadActor = (actorId) => !!db.prepare("SELECT 1 FROM agents WHERE actor_id = ? AND kind = ? AND status = 'running'").get(actorId, LEAD_KIND);
121
- export const LEAD_NAME = LEAD_KIND;
122
- export const isReservedAgentName = (name) => name.toLowerCase() === LEAD_NAME;
123
119
  export function upsertActor(actorId, name, kind) {
124
120
  db.prepare(`INSERT INTO actors (id, name, kind) VALUES (?, ?, ?)
125
121
  ON CONFLICT(id) DO UPDATE SET name = excluded.name, last_seen_at = datetime('now')`).run(actorId, name, kind);
package/dist/todoScope.js CHANGED
@@ -1,3 +1,4 @@
1
+ export const TODO_STATUSES = ["open", "in_progress", "backlog", "completed"];
1
2
  export const ACTIVE_TODO_SQL = "t.status IN ('open', 'in_progress') AND t.archived_at IS NULL";
2
3
  export const NOT_COMPLETED_TODO_SQL = "status != 'completed'";
3
4
  export const OPEN_BLOCKERS_FROM_WHERE = `FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
package/dist/todos.js ADDED
@@ -0,0 +1,90 @@
1
+ import { db } from "./db.js";
2
+ import { matchesAnyTag, parseTags } from "./result.js";
3
+ import { fallbackSlug } from "./slug.js";
4
+ import { OPEN_BLOCKERS_FROM_WHERE } from "./todoScope.js";
5
+ export const COMMENT_COUNT_SQL = `(SELECT COUNT(*) FROM todo_comments c WHERE c.todo_id = t.id)`;
6
+ const SUMMARY_SQL = `
7
+ SELECT t.*,
8
+ (SELECT COUNT(*) ${OPEN_BLOCKERS_FROM_WHERE}) AS open_blockers,
9
+ ${COMMENT_COUNT_SQL} AS comment_count
10
+ FROM todos t`;
11
+ export function getTodo(projectId, todoId) {
12
+ const row = db
13
+ .prepare(`${SUMMARY_SQL} WHERE t.project_id = ? AND t.id = ?`)
14
+ .get(projectId, todoId);
15
+ if (!row)
16
+ throw new Error(`No todo with id ${todoId} in project ${projectId}. Call todo_list.`);
17
+ return row;
18
+ }
19
+ function summarize(row) {
20
+ return {
21
+ todo_id: row.id,
22
+ title: row.title,
23
+ slug: row.slug || fallbackSlug(row.title) || `todo ${row.id}`,
24
+ status: row.status,
25
+ priority: row.priority,
26
+ tags: parseTags(row.tags),
27
+ archived: row.archived_at != null,
28
+ is_blocked: (row.open_blockers ?? 0) > 0,
29
+ open_blockers: row.open_blockers ?? 0,
30
+ comment_count: row.comment_count ?? 0,
31
+ updated_at: row.updated_at,
32
+ };
33
+ }
34
+ export function listTodoSummaries(projectId, filter = {}) {
35
+ const limit = Math.min(filter.limit ?? 50, 200);
36
+ const offset = filter.offset ?? 0;
37
+ let sql = `${SUMMARY_SQL} WHERE t.project_id = ?`;
38
+ const params = [projectId];
39
+ if (!filter.includeArchived) {
40
+ sql += " AND t.archived_at IS NULL";
41
+ }
42
+ if (filter.statuses && filter.statuses.length > 0) {
43
+ sql += ` AND t.status IN (${filter.statuses.map(() => "?").join(",")})`;
44
+ params.push(...filter.statuses);
45
+ }
46
+ if (filter.priority) {
47
+ sql += " AND t.priority = ?";
48
+ params.push(filter.priority);
49
+ }
50
+ if (filter.query) {
51
+ sql += " AND (t.title LIKE ? OR t.body LIKE ? OR t.slug LIKE ?)";
52
+ params.push(`%${filter.query}%`, `%${filter.query}%`, `%${filter.query}%`);
53
+ }
54
+ sql += " ORDER BY t.updated_at DESC";
55
+ let rows = db.prepare(sql).all(...params).filter((r) => matchesAnyTag(r.tags, filter.tags));
56
+ if (filter.isBlocked != null) {
57
+ rows = rows.filter((r) => ((r.open_blockers ?? 0) > 0) === filter.isBlocked);
58
+ }
59
+ return {
60
+ total_count: rows.length,
61
+ offset,
62
+ limit,
63
+ todos: rows.slice(offset, offset + limit).map(summarize),
64
+ };
65
+ }
66
+ export function getTodoDetail(projectId, todoId, includeComments) {
67
+ const todo = getTodo(projectId, todoId);
68
+ const blockers = db
69
+ .prepare(`SELECT t.id, t.title, t.status FROM todo_blockers b
70
+ JOIN todos t ON t.id = b.blocker_id WHERE b.todo_id = ?`)
71
+ .all(todo.id);
72
+ const blocking = db
73
+ .prepare(`SELECT t.id, t.title, t.status FROM todo_blockers b
74
+ JOIN todos t ON t.id = b.todo_id WHERE b.blocker_id = ?`)
75
+ .all(todo.id);
76
+ const result = {
77
+ ...summarize(todo),
78
+ body: todo.body,
79
+ created_at: todo.created_at,
80
+ completed_at: todo.completed_at,
81
+ blockers,
82
+ blocking,
83
+ };
84
+ if (includeComments) {
85
+ result.comments = db
86
+ .prepare("SELECT id, author, body, created_at FROM todo_comments WHERE todo_id = ? ORDER BY created_at")
87
+ .all(todo.id);
88
+ }
89
+ return result;
90
+ }
@@ -2,6 +2,8 @@ import { existsSync, rmSync, statSync, realpathSync } from "node:fs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { z } from "zod";
4
4
  import { db } from "../db.js";
5
+ import { CLOSED_ROW_ORDER, findAgent, getAgentRow, notOwnedError, observeOwnership, PROBE_FAILED_NOTE, summaryLiveness, UNVERIFIED_OWNERSHIP_NOTE } from "../agentRecords.js";
6
+ export { CLOSED_ROW_ORDER, findAgent, getAgentRow, notOwnedError, observeOwnership, probeFailed, PROBE_FAILED_NOTE, REISSUED_PANE_NOTE, summaryLiveness, UNVERIFIED_OWNERSHIP_NOTE } from "../agentRecords.js";
5
7
  import { confirmQueenWrite } from "../queenAudit.js";
6
8
  import { captureQueenWriteIdentity, resolveQueenLeadTarget } from "../context.js";
7
9
  import { agentBriefPath, mergedBriefVars, readAgentBrief, workerBrief, workerCommandString, writeAgentBrief, } from "../brief.js";
@@ -18,21 +20,10 @@ import { agentIdentityEnv, branchAt, closeAgentRow, currentOwnership, discardOrp
18
20
  import { COMMAND_KIND, runningCommandRow, stopLine, stopProcess, STOP_REASONS } from "../processes.js";
19
21
  import { readContextTokens, readContextFill, resolveTranscriptDir } from "../transcript.js";
20
22
  import { leadSessionEnded } from "../leadState.js";
21
- import { capturePane, captureFinalScreen, DEFAULT_LAYOUT, cancelCopyMode, describePaneChoice, ensureAttached, findUnsafeControlChar, holdsHumanInput, liveTargets, observationFailed, observeRowOwnership, ownershipLiveness, paneCurrentCommand, paneInCopyMode, pollPaneReadiness, rowOwnership, sendText, sessionName, setRemainOnExit, sleep, TEXT_ALLOWED_CONTROL_CHARS, tmux, TmuxTimeoutError, WINDOW_LAYOUTS, } from "../tmux.js";
23
+ import { capturePane, captureFinalScreen, DEFAULT_LAYOUT, cancelCopyMode, describePaneChoice, ensureAttached, findUnsafeControlChar, holdsHumanInput, liveTargets, observationFailed, ownershipLiveness, paneCurrentCommand, paneInCopyMode, pollPaneReadiness, rowOwnership, sendText, sessionName, setRemainOnExit, sleep, TEXT_ALLOWED_CONTROL_CHARS, tmux, TmuxTimeoutError, WINDOW_LAYOUTS, } from "../tmux.js";
22
24
  import { agentIdParam, agentNameParam, idParam, projectIdParam } from "./params.js";
23
25
  import { deriveProvenance, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "../stateProvenance.js";
24
- const CLOSED_ROW_ORDER = "(parked_at != '') DESC, closed_at DESC, id DESC";
25
- function closedAgentNamed(projectId, needle) {
26
- return db
27
- .prepare(`SELECT id, name, kind, parked_at FROM agents WHERE project_id = ? AND status != 'running' ORDER BY ${CLOSED_ROW_ORDER}`)
28
- .all(projectId).find((r) => r.name.toLowerCase() === needle);
29
- }
30
- function getAgentRow(projectId, id, notFoundHint) {
31
- const row = db.prepare("SELECT * FROM agents WHERE project_id = ? AND id = ?").get(projectId, id);
32
- if (!row)
33
- throw new Error(`No agent ${id} in project ${projectId}. ${notFoundHint}`);
34
- return row;
35
- }
26
+ const unknownOwnershipNote = (snapshot) => observationFailed(snapshot) ? PROBE_FAILED_NOTE : UNVERIFIED_OWNERSHIP_NOTE;
36
27
  export function classifyLostCas(row) {
37
28
  return row.status !== "running" ? { outcome: "retired", parked: !!row.parked_at } : { outcome: "revived" };
38
29
  }
@@ -42,62 +33,6 @@ function revivedError(agent, live, cause, remedy) {
42
33
  (live ? " after this call's kill-pane took the old one down" : "") +
43
34
  `. ${remedy}`);
44
35
  }
45
- export function findAgent(projectId, ref) {
46
- if (ref.agent_id != null) {
47
- return getAgentRow(projectId, ref.agent_id, "Call agent_list.");
48
- }
49
- if (ref.name) {
50
- const rows = db
51
- .prepare("SELECT * FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
52
- .all(projectId);
53
- const exact = rows.filter((r) => r.name === ref.name);
54
- if (exact.length === 1)
55
- return exact[0];
56
- if (exact.length > 1) {
57
- throw new Error(`Multiple running agents named "${ref.name}". Target by agent_id instead.`);
58
- }
59
- const needle = ref.name.toLowerCase();
60
- const sameName = rows.filter((r) => r.name.toLowerCase() === needle);
61
- if (sameName.length === 1)
62
- return sameName[0];
63
- const closed = closedAgentNamed(projectId, needle);
64
- if (closed) {
65
- if (closed.parked_at) {
66
- throw new Error(`Agent ${closed.id} ("${closed.name}") is PARKED, not finished - it was paused on ` +
67
- `${closed.parked_at} and its session is waiting. Bring it back with agent_resume(agent_id: ` +
68
- `${closed.id}), or abandon the park with agent_close(agent_id: ${closed.id}).`);
69
- }
70
- const remedy = closed.kind === LEAD_KIND ? "Run `hive lead` to start a new one" : "Spawn a new worker";
71
- throw new Error(`Agent ${closed.id} ("${closed.name}") is closed. ${remedy}, or target a running one by name or agent_id.`);
72
- }
73
- const partial = rows.filter((r) => r.name.toLowerCase().includes(needle));
74
- if (partial.length === 1)
75
- return partial[0];
76
- if (partial.length > 1) {
77
- const candidates = partial.map((r) => `${r.name} (agent_id ${r.id})`).join(", ");
78
- throw new Error(`"${ref.name}" matches ${partial.length} running agents: ${candidates}. Use the full name or agent_id.`);
79
- }
80
- if (/^\d+$/.test(ref.name)) {
81
- const id = Number(ref.name);
82
- if (Number.isSafeInteger(id)) {
83
- const byId = db
84
- .prepare("SELECT * FROM agents WHERE project_id = ? AND id = ? AND status = 'running'")
85
- .get(projectId, id);
86
- if (byId)
87
- return byId;
88
- }
89
- }
90
- else if (/^[a-z]+:\d+$/i.test(ref.name)) {
91
- const byActorId = db
92
- .prepare("SELECT * FROM agents WHERE project_id = ? AND actor_id = ? AND status = 'running'")
93
- .get(projectId, ref.name);
94
- if (byActorId)
95
- return byActorId;
96
- }
97
- throw new Error(`No running agent matching "${ref.name}" in project ${projectId}. Call agent_list.`);
98
- }
99
- throw new Error("Pass agent_id or name.");
100
- }
101
36
  function findClosedAgent(projectId, ref) {
102
37
  if (ref.agent_id != null) {
103
38
  const row = getAgentRow(projectId, ref.agent_id, "Call agent_list(include_closed: true).");
@@ -190,28 +125,6 @@ function closeRowOnly(agent, confirmSelf) {
190
125
  (ownership === "unknown" ? " Anything still running in that pane was left alone." : ""),
191
126
  };
192
127
  }
193
- export const PROBE_FAILED_NOTE = "tmux could not be probed, so liveness is unknown. Nothing was changed. Retry in a few seconds.";
194
- const unknownOwnershipNote = (snapshot) => observationFailed(snapshot) ? PROBE_FAILED_NOTE : UNVERIFIED_OWNERSHIP_NOTE;
195
- 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).";
196
- export const probeFailed = (agent) => new Error(`Agent ${agent.id} ("${agent.name}"): ${PROBE_FAILED_NOTE}`);
197
- // A failed probe throws rather than answering unknown, so callers keep the retry wording.
198
- export function observeOwnership(agent) {
199
- const ownership = observeRowOwnership(agent);
200
- if (ownership === "probe-failed")
201
- throw probeFailed(agent);
202
- return ownership;
203
- }
204
- 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.";
205
- export function notOwnedError(agent, ownership) {
206
- const who = `Agent ${agent.id} ("${agent.name}")`;
207
- if (ownership === "gone") {
208
- 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.`);
209
- }
210
- if (ownership === "reissued") {
211
- return new Error(`${who}: pane ownership reads reissued. ${REISSUED_PANE_NOTE} Close the row with agent_close, which retires it without touching that pane.`);
212
- }
213
- return new Error(`${who}: pane ownership reads unknown. ${UNVERIFIED_OWNERSHIP_NOTE}`);
214
- }
215
128
  // Rechecked just before a kill. "changed" skips the kill and lets the row CAS report the race.
216
129
  function stillOwnsPane(agent, action) {
217
130
  const now = currentOwnership(agent.id, paneIdentity(agent));
@@ -327,11 +240,6 @@ function nextWorkerName(projectId) {
327
240
  return `worker-${count + 1}`;
328
241
  }
329
242
  const PANE_READY_MS = Number(process.env.HIVE_SPAWN_READY_MS ?? 45_000);
330
- export function summaryLiveness(row, snapshot) {
331
- if (row.status !== "running")
332
- return false;
333
- return ownershipLiveness(rowOwnership(row, snapshot));
334
- }
335
243
  function capturePaneQuietly(target) {
336
244
  try {
337
245
  return capturePane(target, 15);
@@ -1,89 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { db } from "../db.js";
3
- import { currentActor, effectiveProjectId, resolveProject } from "../context.js";
3
+ import { effectiveProjectId, resolveProject } from "../context.js";
4
4
  import { matchesAnyTag, parseTags, run } from "../result.js";
5
5
  import { idParam, limitParam, offsetParam, projectIdParam } from "./params.js";
6
- function splitsSurrogatePair(content, index) {
7
- if (index <= 0 || index >= content.length)
8
- return false;
9
- const before = content.charCodeAt(index - 1);
10
- const after = content.charCodeAt(index);
11
- return before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
12
- }
13
- function selectPad(projectId, padId, columns) {
14
- const row = db
15
- .prepare(`SELECT ${columns} FROM pads WHERE project_id = ? AND id = ?`)
16
- .get(projectId, padId);
17
- if (!row)
18
- throw new Error(`No pad with id ${padId} in project ${projectId}. Call pad_list.`);
19
- return row;
20
- }
21
- function getPad(projectId, padId) {
22
- return selectPad(projectId, padId, "*");
23
- }
24
- export function getPadMeta(projectId, padId) {
25
- return selectPad(projectId, padId, "id, name, revision, archived");
26
- }
27
- function revisionMismatchError(padId, expected, current) {
28
- if (current == null) {
29
- return new Error(`Pad ${padId} no longer exists; it may have just been deleted by another write. Call pad_list to confirm.`);
30
- }
31
- return new Error(`Revision mismatch for pad ${padId}: expected ${expected}, current ${current}. Re-read with pad_read and retry.`);
32
- }
33
- function currentRevisionOrDeleted(padId) {
34
- const row = db.prepare("SELECT revision FROM pads WHERE id = ?").get(padId);
35
- return row?.revision ?? null;
36
- }
37
- function assertRowChanged(padId, predicateRevision, changed) {
38
- if (!changed) {
39
- throw revisionMismatchError(padId, predicateRevision, currentRevisionOrDeleted(padId));
40
- }
41
- }
42
- function checkRevision(pad, expected, required) {
43
- if (expected == null) {
44
- if (required) {
45
- throw new Error(`expected_revision is required to overwrite pad ${pad.id}. Current revision: ${pad.revision}. Read it first with pad_read.`);
46
- }
47
- return;
48
- }
49
- if (expected !== pad.revision) {
50
- throw revisionMismatchError(pad.id, expected, pad.revision);
51
- }
52
- }
53
- export function bumpPad(padId, predicateRevision, set, ...params) {
54
- const where = predicateRevision != null ? "id = ? AND revision = ?" : "id = ?";
55
- const whereParams = predicateRevision != null ? [padId, predicateRevision] : [padId];
56
- const row = db
57
- .prepare(`UPDATE pads SET ${set}, revision = revision + 1,
58
- updated_by = ?, updated_at = datetime('now') WHERE ${where} RETURNING revision`)
59
- .get(...params, currentActor(), ...whereParams);
60
- assertRowChanged(padId, predicateRevision, row != null);
61
- return row.revision;
62
- }
63
- export const APPEND_WITH_SEPARATOR_SET = "content = content || (CASE WHEN content = '' OR substr(content, -1) = char(10) THEN '' ELSE char(10) END) || ?";
64
- export function getActivePadByName(projectId, name) {
65
- return db
66
- .prepare("SELECT * FROM pads WHERE project_id = ? AND name = ? AND archived = 0")
67
- .get(projectId, name);
68
- }
69
- export function listActivePads(projectId) {
70
- return db
71
- .prepare(`SELECT name, revision, length(content) AS content_length, updated_by, updated_at
72
- FROM pads WHERE project_id = ? AND archived = 0 ORDER BY name`)
73
- .all(projectId);
74
- }
75
- export function overwritePadContent(projectId, padId, content, expectedRevision) {
76
- const pad = getPadMeta(projectId, padId);
77
- checkRevision(pad, expectedRevision, true);
78
- return bumpPad(padId, pad.revision, "content = ?", content);
79
- }
80
- export function createPad(projectId, name, content, tags) {
81
- const row = db
82
- .prepare(`INSERT OR IGNORE INTO pads (project_id, name, content, tags, updated_by)
83
- VALUES (?, ?, ?, ?, ?) RETURNING id`)
84
- .get(projectId, name, content, JSON.stringify(tags), currentActor());
85
- return row?.id ?? null;
86
- }
6
+ import { APPEND_WITH_SEPARATOR_SET, assertRowChanged, bumpPad, checkRevision, createPad, getPad, getPadMeta, splitsSurrogatePair } from "../pads.js";
7
+ export { APPEND_WITH_SEPARATOR_SET, assertRowChanged, bumpPad, checkRevision, createPad, getActivePadByName, getPad, getPadMeta, listActivePads, overwritePadContent, splitsSurrogatePair } from "../pads.js";
87
8
  export function registerPads(server) {
88
9
  server.registerTool("pad_write", {
89
10
  description: "Create a pad, or fully overwrite one by passing pad_id plus expected_revision. Pad names are unique per project. Prefer pad_append/pad_edit for targeted changes.",
@@ -1,12 +1,16 @@
1
1
  import { z } from "zod";
2
2
  import { db } from "../db.js";
3
+ import { getTodo, getTodoDetail, listTodoSummaries } from "../todos.js";
4
+ import { TODO_STATUSES } from "../todoScope.js";
5
+ export { COMMENT_COUNT_SQL, getTodo, getTodoDetail, listTodoSummaries } from "../todos.js";
6
+ export { TODO_STATUSES } from "../todoScope.js";
3
7
  import { commitQueenStateWrite } from "../queenAudit.js";
4
8
  import { currentActor, effectiveProjectId, resolveProject } from "../context.js";
5
- import { matchesAnyTag, parseTags, run } from "../result.js";
9
+ import { run } from "../result.js";
6
10
  import { findUnsafeControlChar } from "../tmux.js";
7
11
  import { SLUG_MAX_LEN, fallbackSlug } from "../slug.js";
8
12
  import { idParam, limitParam, offsetParam, projectIdParam } from "./params.js";
9
- import { NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_FROM_WHERE, OPEN_BLOCKERS_SQL } from "../todoScope.js";
13
+ import { NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_SQL } from "../todoScope.js";
10
14
  const priorityParam = z.enum(["high", "medium", "low"]);
11
15
  const slugParam = z
12
16
  .string()
@@ -18,96 +22,9 @@ const slugParam = z
18
22
  .describe(`Short label, ~3-5 words (${SLUG_MAX_LEN} chars max). When set, it renders beside the id: bracketed in \`hive todos\`, \`hive todo <id>\`, and the session-start digest, and as the row's whole label in the dashboard. Left unset, those surfaces fall back to the title - shown in full in \`hive todos\`/\`hive todo <id>\`/the digest, truncated to this same length in the dashboard. Free text, not a pad-style slug. Pass "" to clear a previously-set slug back to unset.`)
19
23
  .optional();
20
24
  export { SLUG_MAX_LEN, fallbackSlug };
21
- export const TODO_STATUSES = ["open", "in_progress", "backlog", "completed"];
22
25
  const statusParam = z.enum(TODO_STATUSES);
23
26
  const LIVE_DEPENDENTS_SQL = `SELECT t.id AS todo_id FROM todo_blockers b JOIN todos t ON t.id = b.todo_id
24
27
  WHERE b.blocker_id = ? AND t.${NOT_COMPLETED_TODO_SQL} AND t.archived_at IS NULL`;
25
- export const COMMENT_COUNT_SQL = `(SELECT COUNT(*) FROM todo_comments c WHERE c.todo_id = t.id)`;
26
- const SUMMARY_SQL = `
27
- SELECT t.*,
28
- (SELECT COUNT(*) ${OPEN_BLOCKERS_FROM_WHERE}) AS open_blockers,
29
- ${COMMENT_COUNT_SQL} AS comment_count
30
- FROM todos t`;
31
- function getTodo(projectId, todoId) {
32
- const row = db
33
- .prepare(`${SUMMARY_SQL} WHERE t.project_id = ? AND t.id = ?`)
34
- .get(projectId, todoId);
35
- if (!row)
36
- throw new Error(`No todo with id ${todoId} in project ${projectId}. Call todo_list.`);
37
- return row;
38
- }
39
- function summarize(row) {
40
- return {
41
- todo_id: row.id,
42
- title: row.title,
43
- slug: row.slug || fallbackSlug(row.title) || `todo ${row.id}`,
44
- status: row.status,
45
- priority: row.priority,
46
- tags: parseTags(row.tags),
47
- archived: row.archived_at != null,
48
- is_blocked: (row.open_blockers ?? 0) > 0,
49
- open_blockers: row.open_blockers ?? 0,
50
- comment_count: row.comment_count ?? 0,
51
- updated_at: row.updated_at,
52
- };
53
- }
54
- export function listTodoSummaries(projectId, filter = {}) {
55
- const limit = Math.min(filter.limit ?? 50, 200);
56
- const offset = filter.offset ?? 0;
57
- let sql = `${SUMMARY_SQL} WHERE t.project_id = ?`;
58
- const params = [projectId];
59
- if (!filter.includeArchived) {
60
- sql += " AND t.archived_at IS NULL";
61
- }
62
- if (filter.statuses && filter.statuses.length > 0) {
63
- sql += ` AND t.status IN (${filter.statuses.map(() => "?").join(",")})`;
64
- params.push(...filter.statuses);
65
- }
66
- if (filter.priority) {
67
- sql += " AND t.priority = ?";
68
- params.push(filter.priority);
69
- }
70
- if (filter.query) {
71
- sql += " AND (t.title LIKE ? OR t.body LIKE ? OR t.slug LIKE ?)";
72
- params.push(`%${filter.query}%`, `%${filter.query}%`, `%${filter.query}%`);
73
- }
74
- sql += " ORDER BY t.updated_at DESC";
75
- let rows = db.prepare(sql).all(...params).filter((r) => matchesAnyTag(r.tags, filter.tags));
76
- if (filter.isBlocked != null) {
77
- rows = rows.filter((r) => ((r.open_blockers ?? 0) > 0) === filter.isBlocked);
78
- }
79
- return {
80
- total_count: rows.length,
81
- offset,
82
- limit,
83
- todos: rows.slice(offset, offset + limit).map(summarize),
84
- };
85
- }
86
- export function getTodoDetail(projectId, todoId, includeComments) {
87
- const todo = getTodo(projectId, todoId);
88
- const blockers = db
89
- .prepare(`SELECT t.id, t.title, t.status FROM todo_blockers b
90
- JOIN todos t ON t.id = b.blocker_id WHERE b.todo_id = ?`)
91
- .all(todo.id);
92
- const blocking = db
93
- .prepare(`SELECT t.id, t.title, t.status FROM todo_blockers b
94
- JOIN todos t ON t.id = b.todo_id WHERE b.blocker_id = ?`)
95
- .all(todo.id);
96
- const result = {
97
- ...summarize(todo),
98
- body: todo.body,
99
- created_at: todo.created_at,
100
- completed_at: todo.completed_at,
101
- blockers,
102
- blocking,
103
- };
104
- if (includeComments) {
105
- result.comments = db
106
- .prepare("SELECT id, author, body, created_at FROM todo_comments WHERE todo_id = ? ORDER BY created_at")
107
- .all(todo.id);
108
- }
109
- return result;
110
- }
111
28
  function transitiveBlockers(startId) {
112
29
  const seen = new Set();
113
30
  const stack = [startId];
@@ -1,15 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import { db } from "../db.js";
3
+ import { assertSafeWakeBody, resolveAgentRef, resolveWakeDelivery } from "../wakeTarget.js";
4
+ export { assertSafeWakeBody, resolveAgentRef, resolveWakeDelivery } from "../wakeTarget.js";
3
5
  import { commitQueenStateWrite } from "../queenAudit.js";
4
- import { captureQueenWriteIdentity, currentActor, effectiveProjectId, getProject, isQueenLead, resolveQueenLeadTarget, } from "../context.js";
6
+ import { currentActor, effectiveProjectId, getProject, isQueenLead, } from "../context.js";
5
7
  import { run } from "../result.js";
6
- import { findAgent, observeOwnership, notOwnedError, summaryLiveness } from "./agents.js";
7
- import { ACTIVE_TIMER_WHERE, LOG_RETENTION, OWNED_BY_WATCH, WATCH_SCOPE_PROJECT, seedGoneCursor, } from "../scheduler.js";
8
+ import { summaryLiveness } from "../agentRecords.js";
9
+ import { LOG_RETENTION, OWNED_BY_WATCH, WATCH_SCOPE_PROJECT, seedGoneCursor, } from "../scheduler.js";
10
+ import { ACTIVE_TIMER_WHERE } from "../wakeSql.js";
8
11
  import { idParam, projectIdParam } from "./params.js";
9
12
  import { cutToUnitBudget } from "../slug.js";
10
13
  import { awaitingFirstPrompt } from "../firstPrompt.js";
11
14
  import { deriveProvenance } from "../stateProvenance.js";
12
- import { findUnsafeControlChar, foreignSocket, liveTargets, observationFailed, rowOwnership, TEXT_ALLOWED_CONTROL_CHARS, } from "../tmux.js";
15
+ import { foreignSocket, liveTargets, observationFailed, rowOwnership, } from "../tmux.js";
13
16
  import { readLeadTurnState } from "../leadState.js";
14
17
  import { LEAD_TARGET_GONE, LEAD_TARGET_REISSUED, leadSubscription, paneVeto, runningLeadOf } from "../leadWatch.js";
15
18
  import { isRunningLeadActor, LEAD_KIND } from "../spawn.js";
@@ -17,67 +20,6 @@ import { commandHead, screenClassifiable } from "../harnesses.js";
17
20
  const agentRefParam = z
18
21
  .union([idParam, z.string()])
19
22
  .describe("Running agent's name (preferred), or its numeric agent id.");
20
- function resolveAgentRef(projectId, ref) {
21
- return typeof ref === "number"
22
- ? findAgent(projectId, { agent_id: ref })
23
- : findAgent(projectId, { name: ref });
24
- }
25
- function refuseUnclassifiableTarget(agent) {
26
- if (screenClassifiable(agent.command))
27
- return;
28
- throw new Error(`Agent ${agent.id} ("${agent.name}") runs ${JSON.stringify(commandHead(agent.command))}, and hive can only ` +
29
- "classify a claude screen. A wake is delivered by pasting its body and pressing Enter, and the two guards " +
30
- "that decide whether that is safe - a dialog on screen, unsubmitted human text in the box - both read " +
31
- "claude's own chrome, so on this pane neither can answer. Delivery would type blind: into a dialog it " +
32
- "answers the highlighted option, and on a shell it executes whatever the paste merged with. " +
33
- "Refused here rather than at delivery, where it could only ever be held forever - a pane's harness never " +
34
- "becomes classifiable, so that hold would never clear, and a timer that cannot fire is worse than an " +
35
- "error you can read now. Drive this pane with agent_send(keys: [...]) instead, which is unguarded for " +
36
- "exactly this reason.");
37
- }
38
- export function resolveWakeDelivery(projectId, deliverTo, queenOperation) {
39
- if (deliverTo != null) {
40
- const agent = queenOperation
41
- ? resolveQueenLeadTarget(queenOperation, projectId, deliverTo, () => resolveAgentRef(projectId, deliverTo))
42
- : resolveAgentRef(projectId, deliverTo);
43
- // Before the liveness probe: the harness is a durable fact about the row, while liveness is a
44
- // question about right now, so a dead bash worker should say what is actually wrong with it.
45
- refuseUnclassifiableTarget(agent);
46
- const ownership = agent.status === "running" ? observeOwnership(agent) : "gone";
47
- if (ownership === "gone")
48
- throw new Error(`Agent "${agent.name}" has no live tmux window to deliver to.`);
49
- if (ownership !== "live")
50
- throw notOwnedError(agent, ownership);
51
- return { actor: agent.actor_id, pane: agent.tmux_target };
52
- }
53
- const actor = currentActor();
54
- const own = db
55
- .prepare("SELECT * FROM agents WHERE actor_id = ? AND status = 'running' ORDER BY id DESC LIMIT 1")
56
- .get(actor);
57
- if (queenOperation && !own && captureQueenWriteIdentity(projectId)) {
58
- resolveQueenLeadTarget(queenOperation, projectId, deliverTo, () => {
59
- throw new Error("the calling session has no running agent row to receive this wake");
60
- });
61
- }
62
- // Checked on the ROW, not behind the liveness probe: isLive is three-state, and letting a null
63
- // fall through to the TMUX_PANE branch below accepts a wake the scheduler will then hold forever
64
- // against that same row's command - the never-firing timer this refusal exists to prevent.
65
- const target = own && queenOperation ? resolveQueenLeadTarget(queenOperation, projectId, deliverTo, () => own) : own;
66
- if (target)
67
- refuseUnclassifiableTarget(target);
68
- if (target) {
69
- const ownership = observeOwnership(target);
70
- if (ownership === "live")
71
- return { actor, pane: target.tmux_target };
72
- // Unknown never falls through to the ambient pane: that would bind this row's actor to a pane it may not own.
73
- if (ownership === "unknown")
74
- throw notOwnedError(target, ownership);
75
- }
76
- const pane = process.env.TMUX_PANE;
77
- if (pane)
78
- return { actor, pane };
79
- throw new Error("This session cannot receive wake-ups: it is not running inside tmux, so nothing can be typed into its terminal. Start the lead inside tmux (run tmux, then claude; watch with tmux attach, or tmux -CC attach for iTerm's native windows), or pass deliver_to targeting a spawned agent.");
80
- }
81
23
  function pendingWakes(projectId) {
82
24
  return db
83
25
  .prepare(`SELECT * FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} ORDER BY id`)
@@ -138,16 +80,6 @@ function deliveredByNote(t) {
138
80
  : null;
139
81
  }
140
82
  export const truncateBody = (body) => body.length > 120 ? `${cutToUnitBudget(body, 120)}…` : body;
141
- export function assertSafeWakeBody(body) {
142
- const bad = findUnsafeControlChar(body, TEXT_ALLOWED_CONTROL_CHARS);
143
- if (bad) {
144
- throw new Error(`body cannot contain ${bad.label} at offset ${bad.index}: it is typed literally into the target pane ` +
145
- "when the wake fires, and a raw control byte reaches tmux as a keystroke instead of as text, " +
146
- "silently turning the delivery into a keys call nobody chose. Tab and newline are the only control " +
147
- "characters allowed - every wake in this project is multi-line prose. To send an actual keystroke " +
148
- 'on purpose, call agent_send(keys: [...]) against the target directly instead of scheduling it here.');
149
- }
150
- }
151
83
  const baseWakeFields = (t, { truncate = true } = {}) => ({
152
84
  wake_id: t.id,
153
85
  kind: t.kind,
@@ -0,0 +1 @@
1
+ export const ACTIVE_TIMER_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
@@ -0,0 +1,76 @@
1
+ import { db } from "./db.js";
2
+ import { captureQueenWriteIdentity, currentActor, resolveQueenLeadTarget } from "./context.js";
3
+ import { findAgent, observeOwnership, notOwnedError } from "./agentRecords.js";
4
+ import { commandHead, screenClassifiable } from "./harnesses.js";
5
+ import { findUnsafeControlChar, TEXT_ALLOWED_CONTROL_CHARS } from "./tmux.js";
6
+ export function resolveAgentRef(projectId, ref) {
7
+ return typeof ref === "number"
8
+ ? findAgent(projectId, { agent_id: ref })
9
+ : findAgent(projectId, { name: ref });
10
+ }
11
+ function refuseUnclassifiableTarget(agent) {
12
+ if (screenClassifiable(agent.command))
13
+ return;
14
+ throw new Error(`Agent ${agent.id} ("${agent.name}") runs ${JSON.stringify(commandHead(agent.command))}, and hive can only ` +
15
+ "classify a claude screen. A wake is delivered by pasting its body and pressing Enter, and the two guards " +
16
+ "that decide whether that is safe - a dialog on screen, unsubmitted human text in the box - both read " +
17
+ "claude's own chrome, so on this pane neither can answer. Delivery would type blind: into a dialog it " +
18
+ "answers the highlighted option, and on a shell it executes whatever the paste merged with. " +
19
+ "Refused here rather than at delivery, where it could only ever be held forever - a pane's harness never " +
20
+ "becomes classifiable, so that hold would never clear, and a timer that cannot fire is worse than an " +
21
+ "error you can read now. Drive this pane with agent_send(keys: [...]) instead, which is unguarded for " +
22
+ "exactly this reason.");
23
+ }
24
+ export function resolveWakeDelivery(projectId, deliverTo, queenOperation) {
25
+ if (deliverTo != null) {
26
+ const agent = queenOperation
27
+ ? resolveQueenLeadTarget(queenOperation, projectId, deliverTo, () => resolveAgentRef(projectId, deliverTo))
28
+ : resolveAgentRef(projectId, deliverTo);
29
+ // Before the liveness probe: the harness is a durable fact about the row, while liveness is a
30
+ // question about right now, so a dead bash worker should say what is actually wrong with it.
31
+ refuseUnclassifiableTarget(agent);
32
+ const ownership = agent.status === "running" ? observeOwnership(agent) : "gone";
33
+ if (ownership === "gone")
34
+ throw new Error(`Agent "${agent.name}" has no live tmux window to deliver to.`);
35
+ if (ownership !== "live")
36
+ throw notOwnedError(agent, ownership);
37
+ return { actor: agent.actor_id, pane: agent.tmux_target };
38
+ }
39
+ const actor = currentActor();
40
+ const own = db
41
+ .prepare("SELECT * FROM agents WHERE actor_id = ? AND status = 'running' ORDER BY id DESC LIMIT 1")
42
+ .get(actor);
43
+ if (queenOperation && !own && captureQueenWriteIdentity(projectId)) {
44
+ resolveQueenLeadTarget(queenOperation, projectId, deliverTo, () => {
45
+ throw new Error("the calling session has no running agent row to receive this wake");
46
+ });
47
+ }
48
+ // Checked on the ROW, not behind the liveness probe: isLive is three-state, and letting a null
49
+ // fall through to the TMUX_PANE branch below accepts a wake the scheduler will then hold forever
50
+ // against that same row's command - the never-firing timer this refusal exists to prevent.
51
+ const target = own && queenOperation ? resolveQueenLeadTarget(queenOperation, projectId, deliverTo, () => own) : own;
52
+ if (target)
53
+ refuseUnclassifiableTarget(target);
54
+ if (target) {
55
+ const ownership = observeOwnership(target);
56
+ if (ownership === "live")
57
+ return { actor, pane: target.tmux_target };
58
+ // Unknown never falls through to the ambient pane: that would bind this row's actor to a pane it may not own.
59
+ if (ownership === "unknown")
60
+ throw notOwnedError(target, ownership);
61
+ }
62
+ const pane = process.env.TMUX_PANE;
63
+ if (pane)
64
+ return { actor, pane };
65
+ throw new Error("This session cannot receive wake-ups: it is not running inside tmux, so nothing can be typed into its terminal. Start the lead inside tmux (run tmux, then claude; watch with tmux attach, or tmux -CC attach for iTerm's native windows), or pass deliver_to targeting a spawned agent.");
66
+ }
67
+ export function assertSafeWakeBody(body) {
68
+ const bad = findUnsafeControlChar(body, TEXT_ALLOWED_CONTROL_CHARS);
69
+ if (bad) {
70
+ throw new Error(`body cannot contain ${bad.label} at offset ${bad.index}: it is typed literally into the target pane ` +
71
+ "when the wake fires, and a raw control byte reaches tmux as a keystroke instead of as text, " +
72
+ "silently turning the delivery into a keys call nobody chose. Tab and newline are the only control " +
73
+ "characters allowed - every wake in this project is multi-line prose. To send an actual keystroke " +
74
+ 'on purpose, call agent_send(keys: [...]) against the target directly instead of scheduling it here.');
75
+ }
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmgmyr/hive",
3
- "version": "1.11.5",
3
+ "version": "1.11.6",
4
4
  "description": "Run a crew of Claude Code and Codex workers from one lead session: visible tmux panes, exact state from hooks, one local SQLite store.",
5
5
  "type": "module",
6
6
  "bin": {