@cmgmyr/hive 1.11.4 → 1.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":"1.11.4","sha":"be85c43","dirty":false,"build_id":"002d81642b9604f453a66605e3bc23656ad7b425069e016be8bfda87d6e532d1"}
1
+ {"version":"1.11.5","sha":"486161d","dirty":false,"build_id":"3a33da919c1ac52f423bfdb530cfeda01207921e9c0fb8b262462fae1c266b12"}
package/dist/cli.js CHANGED
@@ -36,7 +36,7 @@ import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchA
36
36
  import { adoptableWindow, applyLayout, claimInitialWindow, configureHiveWindow, controlModeFor, createWindow, DEFAULT_LAYOUT, describePaneChoice, ensureSession, findProjectWindow, foreignSocket, isViewSessionName, listOwnedWindows, liveTargets, observationFailed, observeRowOwnership, ORPHAN_MIN_AGE_MS, orphanScratchServers, orphansWorthWarningAbout, paneIsAloneInWindow, paneVisibility, panePidForRecord, PROCESSES_LAYOUT, processesPaneTitle, processesWindowName, projectWindows, RAW_ATTACH_TMUX_CONFIG, renderAttachCommand, resolveAttachTarget, resolveInTmuxTarget, rowLive, rowOwnership, SESSION_PREFIX, appendGlobalHook, globalHooks, replaceGlobalHooks, sessionName, setPaneTitle, shellQuote, shownPaneTitle, makeProcessesWindow, tmux, TMUX_DOC, tmuxSaysNothingThere, tmuxTimeoutOverride, tmuxSocketPath, untrustedTmuxServer, waitForPaneEstablished, windowLayout, } from "./tmux.js";
37
37
  import { collectCrew, crewStoreProblem } from "./crew.js";
38
38
  import { describeVisibility, formatLocal, processCounts } from "./dashboard.js";
39
- import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, STOP_REASONS, } from "./processes.js";
39
+ import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, renderStart, STOP_REASONS, } from "./processes.js";
40
40
  import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
41
41
  import { appendClaudeLeadArgs, crewPluginDir, isTrusted, leadIdentityEnv, renderLeadPosture } from "./leadLaunch.js";
42
42
  import { handoffStatusSegment } from "./leadHandoff.js";
@@ -47,7 +47,8 @@ import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
47
47
  import { ageSecondsSince, deriveProvenance, describeForHuman, describeLastLogEvent, humanizeAge, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "./stateProvenance.js";
48
48
  import { awaitingFirstPromptSql } from "./firstPrompt.js";
49
49
  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, OPEN_BLOCKERS_SQL, TODO_STATUSES, } from "./tools/todos.js";
50
+ import { getTodoDetail, listTodoSummaries, TODO_STATUSES, } from "./tools/todos.js";
51
+ import { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
51
52
  import { createPad, getActivePadByName, listActivePads, overwritePadContent, } from "./tools/pads.js";
52
53
  import { answerQuestion, listQuestions } from "./questions.js";
53
54
  function usage() {
@@ -213,9 +214,15 @@ async function ensureTrusted(projectId, name, command, dir, env) {
213
214
  db.prepare("INSERT OR IGNORE INTO command_trust (project_id, name, config_hash) VALUES (?, ?, ?)").run(projectId, name, hash);
214
215
  return true;
215
216
  }
216
- function startYmlCommand(project, name, proc, config) {
217
+ function startYmlCommand(project, name, proc, config, trusted) {
218
+ if (!trusted)
219
+ return { status: "refused", reason: "untrusted" };
217
220
  if (isReservedAgentName(name)) {
218
- return `skipped: "lead" is reserved for this project's lead session and cannot be used as a process name`;
221
+ return {
222
+ status: "refused",
223
+ reason: "reserved-name",
224
+ message: `skipped: "lead" is reserved for this project's lead session and cannot be used as a process name`,
225
+ };
219
226
  }
220
227
  const existing = db
221
228
  .prepare("SELECT id, tmux_target, tmux_socket FROM agents WHERE project_id = ? AND name = ? AND status = 'running'")
@@ -225,10 +232,15 @@ function startYmlCommand(project, name, proc, config) {
225
232
  if (live) {
226
233
  const windows = projectWindows(sessionName(), project.id);
227
234
  const where = windows === null ? null : paneVisibility(existing.tmux_target, windows);
228
- return where === "shown" || where === "hidden" ? `already running (${where})` : "already running";
235
+ return { status: "already-running", location: where === "shown" || where === "hidden" ? where : null };
236
+ }
237
+ if (live === null) {
238
+ return {
239
+ status: "refused",
240
+ reason: "unknown-liveness",
241
+ message: "skipped: tmux could not be probed, so hive cannot tell whether it is already running",
242
+ };
229
243
  }
230
- if (live === null)
231
- return "skipped: tmux could not be probed, so hive cannot tell whether it is already running";
232
244
  closeAgentRow(existing.id);
233
245
  }
234
246
  let dir;
@@ -236,7 +248,7 @@ function startYmlCommand(project, name, proc, config) {
236
248
  dir = resolveCommandDir(project.path, proc.dir);
237
249
  }
238
250
  catch (e) {
239
- return `skipped: ${errorMessage(e)}`;
251
+ return { status: "refused", reason: "escaped-dir", message: `skipped: ${errorMessage(e)}` };
240
252
  }
241
253
  const placement = proc.visible
242
254
  ? (config.placement ?? (process.env.HIVE_SPAWN_PLACEMENT === "window" ? "window" : "split"))
@@ -260,17 +272,20 @@ function startYmlCommand(project, name, proc, config) {
260
272
  setPaneTitle(target, shownPaneTitle(project.name, name));
261
273
  if (landedInProjectId != null) {
262
274
  const landedName = getProject(landedInProjectId)?.name ?? `project ${landedInProjectId}`;
263
- return `started, landed in project "${landedName}"'s window`;
275
+ return { status: "started", message: `started, landed in project "${landedName}"'s window` };
264
276
  }
265
- return "started";
277
+ return { status: "started", message: "started" };
266
278
  }
267
- return inProcessesWindow
268
- ? "started (hidden)"
269
- : "started in its own window: it opened this project's tmux session, and a session's first window " +
270
- `cannot be a tile. Move it with: hive hide "${name}"`;
279
+ return {
280
+ status: "started",
281
+ message: inProcessesWindow
282
+ ? "started (hidden)"
283
+ : "started in its own window: it opened this project's tmux session, and a session's first window " +
284
+ `cannot be a tile. Move it with: hive hide "${name}"`,
285
+ };
271
286
  }
272
287
  catch (e) {
273
- return `failed: ${errorMessage(e)}`;
288
+ return { status: "failed", message: errorMessage(e) };
274
289
  }
275
290
  }
276
291
  function attach(session, project, window) {
@@ -636,9 +651,10 @@ async function cmdLead(argv) {
636
651
  console.log(`- ${name}: defined, auto_start off (start with: hive start "${name}")`);
637
652
  continue;
638
653
  }
639
- if (await ensureTrusted(project.id, name, proc.command, proc.dir, proc.env)) {
640
- console.log(`- ${name}: ${startYmlCommand(project, name, proc, config)}`);
641
- }
654
+ const trusted = await ensureTrusted(project.id, name, proc.command, proc.dir, proc.env);
655
+ const result = startYmlCommand(project, name, proc, config, trusted);
656
+ if (trusted)
657
+ console.log(`- ${name}: ${renderStart(result)}`);
642
658
  }
643
659
  }
644
660
  if (registrationNotice) {
@@ -1366,9 +1382,12 @@ async function cmdStart(argv) {
1366
1382
  console.log(processNotDefined(config, name));
1367
1383
  process.exit(1);
1368
1384
  }
1369
- if (await ensureTrusted(project.id, name, proc.command, proc.dir, proc.env)) {
1370
- console.log(`${name}: ${startYmlCommand(project, name, proc, config)}`);
1371
- }
1385
+ const trusted = await ensureTrusted(project.id, name, proc.command, proc.dir, proc.env);
1386
+ const result = startYmlCommand(project, name, proc, config, trusted);
1387
+ if (trusted)
1388
+ console.log(`${name}: ${renderStart(result)}`);
1389
+ if (!trusted || result.status === "failed" || result.status === "refused")
1390
+ process.exitCode = 1;
1372
1391
  }
1373
1392
  function processNotDefined(config, name) {
1374
1393
  const known = Object.keys(config?.processes ?? {});
@@ -1631,7 +1650,7 @@ function cmdStatus() {
1631
1650
  .prepare("SELECT * FROM agents WHERE project_id = ? AND status = 'running' ORDER BY kind DESC, id")
1632
1651
  .all(project.id);
1633
1652
  const todos = db
1634
- .prepare("SELECT COUNT(*) AS n FROM todos WHERE project_id = ? AND status IN ('open', 'in_progress') AND archived_at IS NULL")
1653
+ .prepare(`SELECT COUNT(*) AS n FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}`)
1635
1654
  .get(project.id).n;
1636
1655
  const { wakes, heldWakes } = db
1637
1656
  .prepare(`SELECT COUNT(*) AS wakes, COUNT(held_at) AS heldWakes FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}`)
@@ -2769,9 +2788,9 @@ function cmdStatusline() {
2769
2788
  const count = (sql) => db.prepare(sql).get(project.id).n;
2770
2789
  const agents = count("SELECT COUNT(*) AS n FROM agents WHERE project_id = ? AND status = 'running' AND kind = 'agent'");
2771
2790
  const commands = count("SELECT COUNT(*) AS n FROM agents WHERE project_id = ? AND status = 'running' AND kind = 'command'");
2772
- const todos = count("SELECT COUNT(*) AS n FROM todos WHERE project_id = ? AND status IN ('open', 'in_progress') AND archived_at IS NULL");
2791
+ const todos = count(`SELECT COUNT(*) AS n FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}`);
2773
2792
  const ready = count(`SELECT COUNT(*) AS n FROM todos t
2774
- WHERE t.project_id = ? AND t.status IN ('open', 'in_progress') AND t.archived_at IS NULL
2793
+ WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
2775
2794
  AND NOT EXISTS (${OPEN_BLOCKERS_SQL})`);
2776
2795
  const pads = count("SELECT COUNT(*) AS n FROM pads WHERE project_id = ? AND archived = 0");
2777
2796
  const wakes = count(`SELECT COUNT(*) AS n FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}`);
package/dist/codexHome.js CHANGED
@@ -303,7 +303,6 @@ export function ensureCodexHome(input) {
303
303
  indexJs,
304
304
  serverEnv: { ...input.serverEnv, HIVE_AGENT_ID: input.actorId },
305
305
  statusLine,
306
- readOnly: input.readOnly,
307
306
  modelContextWindow,
308
307
  modelAutoCompactTokenLimit,
309
308
  }));
package/dist/context.js CHANGED
@@ -1,4 +1,3 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { lstatSync, realpathSync } from "node:fs";
3
2
  import { userInfo } from "node:os";
4
3
  import { basename, dirname, join, resolve, sep } from "node:path";
@@ -7,6 +6,7 @@ import { db } from "./db.js";
7
6
  import { LEAD_KIND } from "./spawn.js";
8
7
  import { rowOwnership } from "./tmux.js";
9
8
  import { recordQueenRefusal } from "./queenAudit.js";
9
+ import { gitOut } from "./git.js";
10
10
  let selectedId = null;
11
11
  let cachedActorId = null;
12
12
  let lastTouchMs = 0;
@@ -92,18 +92,13 @@ export function registeredAncestor(dir) {
92
92
  return bestPrefixMatch(projects, dir);
93
93
  }
94
94
  export function gitPrimaryRoot(dir) {
95
+ const commonDir = gitOut(dir, ["rev-parse", "--git-common-dir"]);
96
+ if (commonDir === null)
97
+ return null;
98
+ const resolvedCommonDir = resolve(dir, commonDir);
99
+ if (basename(resolvedCommonDir) !== ".git")
100
+ return null;
95
101
  try {
96
- const { GIT_DIR: _gitDir, GIT_COMMON_DIR: _gitCommonDir, GIT_WORK_TREE: _gitWorkTree, ...env } = process.env;
97
- const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
98
- cwd: dir,
99
- encoding: "utf8",
100
- stdio: ["ignore", "pipe", "ignore"],
101
- timeout: 2000,
102
- env,
103
- }).trim();
104
- const resolvedCommonDir = resolve(dir, commonDir);
105
- if (basename(resolvedCommonDir) !== ".git")
106
- return null;
107
102
  return realpathSync(dirname(resolvedCommonDir));
108
103
  }
109
104
  catch {
@@ -111,19 +106,13 @@ export function gitPrimaryRoot(dir) {
111
106
  }
112
107
  }
113
108
  function gitDirs(dir) {
109
+ const result = gitOut(dir, ["rev-parse", "--git-dir", "--git-common-dir"]);
110
+ if (result === null)
111
+ return null;
112
+ const out = result.split("\n");
113
+ if (out.length !== 2)
114
+ return null;
114
115
  try {
115
- const { GIT_DIR: _gitDir, GIT_COMMON_DIR: _gitCommonDir, GIT_WORK_TREE: _gitWorkTree, ...env } = process.env;
116
- const out = execFileSync("git", ["rev-parse", "--git-dir", "--git-common-dir"], {
117
- cwd: dir,
118
- encoding: "utf8",
119
- stdio: ["ignore", "pipe", "ignore"],
120
- timeout: 2000,
121
- env,
122
- })
123
- .trim()
124
- .split("\n");
125
- if (out.length !== 2)
126
- return null;
127
116
  return {
128
117
  gitDir: realpathSync(resolve(dir, out[0])),
129
118
  commonDir: realpathSync(resolve(dir, out[1])),
package/dist/crew.js CHANGED
@@ -1,15 +1,16 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { db } from "./db.js";
3
2
  import { isLinkedWorktree } from "./context.js";
4
3
  import { awaitingFirstPrompt } from "./firstPrompt.js";
5
4
  import { heldReasonLabel } from "./heldLabel.js";
6
5
  import { harnessFor, paneClassifierFor } from "./harnesses.js";
7
6
  import { NEEDS_HUMAN_TAG } from "./portfolio.js";
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
11
  import { ACTIVE_TIMER_WHERE } from "./scheduler.js";
12
12
  import { reportsAgentStateLog } from "./stateProvenance.js";
13
+ import { gitOut } from "./git.js";
13
14
  import { liveTargets, rowOwnership } from "./tmux.js";
14
15
  import { SKILL_TOOL, lastComponent, readContextFill, readRecentToolCalls, } from "./transcript.js";
15
16
  export function crewStoreProblem(database = db) {
@@ -50,36 +51,18 @@ function oneShotWatched(pending) {
50
51
  }
51
52
  });
52
53
  }
53
- function stripped(env) {
54
- const { GIT_DIR: _gitDir, GIT_COMMON_DIR: _gitCommonDir, GIT_WORK_TREE: _gitWorkTree, ...rest } = env;
55
- return rest;
56
- }
57
- function git(cwd, args) {
58
- try {
59
- return execFileSync("git", args, {
60
- cwd,
61
- encoding: "utf8",
62
- stdio: ["ignore", "pipe", "ignore"],
63
- timeout: 2000,
64
- env: stripped(process.env),
65
- }).trim();
66
- }
67
- catch {
68
- return null;
69
- }
70
- }
71
54
  export function readCommitsAhead(cwd, projectRoot) {
72
55
  if (!cwd || cwd === projectRoot || !isLinkedWorktree(cwd))
73
56
  return null;
74
57
  const candidates = [];
75
- const remoteHead = git(cwd, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
58
+ const remoteHead = gitOut(cwd, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
76
59
  if (remoteHead)
77
60
  candidates.push(remoteHead);
78
61
  candidates.push("refs/heads/main", "refs/heads/master");
79
62
  for (const ref of candidates) {
80
- if (git(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]) === null)
63
+ if (gitOut(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]) === null)
81
64
  continue;
82
- const count = git(cwd, ["rev-list", "--count", `${ref}..HEAD`]);
65
+ const count = gitOut(cwd, ["rev-list", "--count", `${ref}..HEAD`]);
83
66
  return count !== null && /^\d+$/.test(count) ? Number(count) : null;
84
67
  }
85
68
  return null;
@@ -481,7 +464,7 @@ export function collectCrew(project, now) {
481
464
  if (!staffed.has(todo.id))
482
465
  lanes.push({ todo, worker: null });
483
466
  const needsYou = db
484
- .prepare("SELECT id, slug, tags FROM todos WHERE project_id = ? AND status != 'completed' AND archived_at IS NULL ORDER BY id")
467
+ .prepare(`SELECT t.id, t.slug, t.tags FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL} ORDER BY t.id`)
485
468
  .all(project.id)
486
469
  .filter((t) => parseTags(t.tags).includes(NEEDS_HUMAN_TAG))
487
470
  .map((t) => ({ id: t.id, slug: t.slug }));
package/dist/dashboard.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { createRequire } from "node:module";
3
3
  import { db } from "./db.js";
4
+ import { ACTIVE_TODO_SQL, NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
4
5
  import { getProject } from "./context.js";
5
6
  export const describeVisibility = (visibility) => visibility === null ? "running" : visibility === "window" ? "own window" : visibility;
6
7
  export function processCounts(procs) {
@@ -127,12 +128,9 @@ function fetchNextWake(projectId) {
127
128
  function fetchTodoCounts(projectId) {
128
129
  return db
129
130
  .prepare(`SELECT
130
- (SELECT COUNT(*) FROM todos WHERE project_id = ? AND archived_at IS NULL AND status != 'completed') AS open,
131
- (SELECT COUNT(*) FROM todos t WHERE t.project_id = ? AND t.archived_at IS NULL AND t.status != 'completed'
132
- AND EXISTS (
133
- SELECT 1 FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
134
- WHERE b.todo_id = t.id AND bt.status != 'completed'
135
- )) AS blocked,
131
+ (SELECT COUNT(*) FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}) AS open,
132
+ (SELECT COUNT(*) FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
133
+ AND EXISTS (${OPEN_BLOCKERS_SQL})) AS blocked,
136
134
  (SELECT COUNT(*) FROM todos
137
135
  WHERE project_id = ? AND completed_at IS NOT NULL
138
136
  AND date(completed_at, 'localtime') >= date('now', 'localtime', '-6 days')) AS completed7d`)
@@ -262,10 +260,9 @@ function renderBoardSection(index, projectId) {
262
260
  }
263
261
  const OPEN_TODOS_SQL = `
264
262
  SELECT t.id, t.title, t.body, t.slug, t.status, t.priority,
265
- (SELECT COUNT(*) FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
266
- WHERE b.todo_id = t.id AND bt.status != 'completed') AS open_blockers
263
+ EXISTS (${OPEN_BLOCKERS_SQL}) AS blocked
267
264
  FROM todos t
268
- WHERE t.project_id = ? AND t.archived_at IS NULL AND t.status != 'completed'
265
+ WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
269
266
  ORDER BY CASE t.priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, t.id`;
270
267
  function fetchBlockersFor(todoIds) {
271
268
  const byTodo = new Map();
@@ -274,7 +271,7 @@ function fetchBlockersFor(todoIds) {
274
271
  const placeholders = todoIds.map(() => "?").join(",");
275
272
  const rows = db
276
273
  .prepare(`SELECT b.todo_id, t.id, t.title FROM todo_blockers b JOIN todos t ON t.id = b.blocker_id
277
- WHERE b.todo_id IN (${placeholders}) AND t.status != 'completed'`)
274
+ WHERE b.todo_id IN (${placeholders}) AND t.${NOT_COMPLETED_TODO_SQL}`)
278
275
  .all(...todoIds);
279
276
  for (const row of rows) {
280
277
  const list = byTodo.get(row.todo_id) ?? [];
@@ -291,11 +288,11 @@ function todoBadgeLevel(status, blocked) {
291
288
  function renderTodosSection(index, projectId) {
292
289
  const all = db.prepare(OPEN_TODOS_SQL).all(projectId);
293
290
  const shown = all.slice(0, TODO_CAP);
294
- const blockersByTodo = fetchBlockersFor(shown.filter((t) => t.open_blockers > 0).map((t) => t.id));
295
- const blockedCount = all.filter((t) => t.open_blockers > 0).length;
291
+ const blockersByTodo = fetchBlockersFor(shown.filter((t) => t.blocked).map((t) => t.id));
292
+ const blockedCount = all.filter((t) => t.blocked).length;
296
293
  const rows = shown
297
294
  .map((t) => {
298
- const blocked = t.open_blockers > 0;
295
+ const blocked = Boolean(t.blocked);
299
296
  const blockers = blocked
300
297
  ? (blockersByTodo.get(t.id) ?? []).map((b) => `${mono(`#${b.id}`)} ${prose(b.title)}`).join(", ")
301
298
  : "";
@@ -324,7 +321,7 @@ function renderTodosSection(index, projectId) {
324
321
  `<p class="filter-none" id="todo-filter-none" hidden>No open todo matches that filter.</p>`
325
322
  : "";
326
323
  const body = filter +
327
- renderCappedList(shown.length, all.length, "open todos", "todos", rows, emptyState("No open todos.", "Every todo in this project is completed or archived. New ones land here."), "todo-list");
324
+ renderCappedList(shown.length, all.length, "open todos", "todos", rows, emptyState("No open todos.", "No active todos are listed. New open or in-progress todos land here. Backlog todos stay off this list."), "todo-list");
328
325
  return section(index, "todos", "Todos", `${all.length} open, ${blockedCount} blocked`, "checklist", String(shown.length), body);
329
326
  }
330
327
  export const CHART_DAYS = 7;
package/dist/db.js CHANGED
@@ -576,6 +576,42 @@ ALTER TABLE queen_audit ADD COLUMN problem TEXT NOT NULL DEFAULT '';
576
576
  `,
577
577
  `
578
578
  ALTER TABLE agents ADD COLUMN pane_started_at TEXT;
579
+ `,
580
+ `
581
+ DROP TRIGGER fence_handoff_held_claim;
582
+ CREATE TRIGGER fence_handoff_held_claim
583
+ BEFORE UPDATE OF fire_count, delivery_method ON wakes
584
+ FOR EACH ROW
585
+ WHEN (NEW.fire_count > OLD.fire_count
586
+ OR (NEW.delivery_method IS 'pty-after-socket-timeout'
587
+ AND OLD.delivery_method IS NOT 'pty-after-socket-timeout'
588
+ AND NEW.typed_at IS NULL))
589
+ AND EXISTS (
590
+ SELECT 1 FROM lead_handoffs h
591
+ WHERE h.lead_agent_id = (SELECT a.id FROM agents a
592
+ WHERE a.actor_id = NEW.deliver_actor
593
+ ORDER BY (a.status = 'running') DESC,
594
+ a.id DESC
595
+ LIMIT 1)
596
+ AND h.state NOT IN ('completed', 'failed')
597
+ AND h.request_wake_id IS NOT NEW.id
598
+ AND (
599
+ (h.state = 'grace'
600
+ AND h.hold_released_at IS NULL
601
+ AND h.grace_started_at IS NOT NULL
602
+ AND (julianday('now') - julianday(h.grace_started_at)) * 86400 < 900)
603
+ OR ((h.state IN ('grace', 'started')
604
+ OR (h.hold_released_at IS NULL AND (
605
+ h.state IN ('respawning', 'ambiguous', 'wind_down')
606
+ OR (h.state = 'requested'
607
+ AND (h.reason = 'stop' OR h.pass >= 2))
608
+ OR (h.state = 'postponed' AND h.reason = 'stop'))))
609
+ AND h.state <> 'grace'
610
+ AND (julianday('now') - julianday(h.updated_at)) * 86400 < 3600)
611
+ ))
612
+ BEGIN
613
+ SELECT RAISE(IGNORE);
614
+ END;
579
615
  `,
580
616
  ];
581
617
  export function storeSchemaAhead(database) {
package/dist/git.js ADDED
@@ -0,0 +1,16 @@
1
+ import { execFileSync } from "node:child_process";
2
+ export function gitOut(cwd, args) {
3
+ const { GIT_DIR: _gitDir, GIT_COMMON_DIR: _gitCommonDir, GIT_WORK_TREE: _gitWorkTree, ...env } = process.env;
4
+ try {
5
+ return execFileSync("git", args, {
6
+ cwd,
7
+ encoding: "utf8",
8
+ stdio: ["ignore", "pipe", "ignore"],
9
+ timeout: 2000,
10
+ env,
11
+ }).trim();
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
package/dist/harnesses.js CHANGED
@@ -53,7 +53,6 @@ const claudeHarness = {
53
53
  supportsRename: true,
54
54
  classifiesPaneScreen: true,
55
55
  paneClassifier: { choiceCheck: paneChoiceCheck, inputBoxState, inputBoxSnapshot, hasInputBox: paneHasInputBox, trailerView: claudeTrailerView },
56
- hasScopes: true,
57
56
  needsHome: false,
58
57
  // "--" so a message that starts with a dash is never read as a flag.
59
58
  initialPromptArgs: (message) => ["--", message],
@@ -82,7 +81,6 @@ export const codexHarness = {
82
81
  hasInputBox: codexPaneHasInputBox,
83
82
  trailerView: codexTrailerView,
84
83
  },
85
- hasScopes: false,
86
84
  needsHome: true,
87
85
  // Auto-submits with no Enter; verified live on v0.149.0.
88
86
  initialPromptArgs: (message) => ["--", message],
@@ -106,7 +104,6 @@ const unknownHarness = {
106
104
  supportsRename: false,
107
105
  classifiesPaneScreen: false,
108
106
  paneClassifier: null,
109
- hasScopes: false,
110
107
  needsHome: false,
111
108
  initialPromptArgs: null,
112
109
  // An unrecognized command's vocabulary is unknown, so it stops nothing rather than guessing.
package/dist/kickoff.js CHANGED
@@ -1,22 +1,13 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { realpathSync } from "node:fs";
3
2
  import { fileURLToPath } from "node:url";
4
3
  import { cutToUnitBudget } from "./slug.js";
5
4
  import { FIRST_MESSAGE_SHA_ENV } from "./firstMessage.js";
5
+ import { gitOut } from "./git.js";
6
6
  export const OUTPUT_BUDGET = 10_000;
7
7
  const CONTEXT_BUDGET = 6_000;
8
8
  const BOARD_BUDGET = 1_800;
9
9
  function currentBranch(dir) {
10
- try {
11
- return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
12
- cwd: dir,
13
- encoding: "utf8",
14
- stdio: ["ignore", "pipe", "ignore"],
15
- }).trim();
16
- }
17
- catch {
18
- return null;
19
- }
10
+ return gitOut(dir, ["rev-parse", "--abbrev-ref", "HEAD"]);
20
11
  }
21
12
  export function truncate(text, limit) {
22
13
  if (text.length <= limit)
@@ -28,7 +19,7 @@ async function digest(projectPath, profile, warnings) {
28
19
  const { findProjectForCwd } = await import("./context.js");
29
20
  const { ACTIVE_TIMER_WHERE } = await import("./scheduler.js");
30
21
  const { getActivePadByName } = await import("./tools/pads.js");
31
- const { OPEN_BLOCKERS_SQL } = await import("./tools/todos.js");
22
+ const { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } = await import("./todoScope.js");
32
23
  migrate();
33
24
  const project = findProjectForCwd();
34
25
  if (!project || project.path !== projectPath)
@@ -53,7 +44,7 @@ async function digest(projectPath, profile, warnings) {
53
44
  .all(project.id);
54
45
  const blocked = db
55
46
  .prepare(`SELECT COUNT(*) AS n FROM todos t
56
- WHERE t.project_id = ? AND t.status IN ('open', 'in_progress') AND t.archived_at IS NULL
47
+ WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
57
48
  AND EXISTS (${OPEN_BLOCKERS_SQL})`)
58
49
  .get(project.id).n;
59
50
  if (inFlight.length > 0) {
@@ -30,7 +30,7 @@ export function readActiveHandoff(leadAgentId) {
30
30
  export function handoffHoldsAutomation(row) {
31
31
  switch (row.state) {
32
32
  case "grace":
33
- return true;
33
+ return row.hold_released_at === null && !olderThan(row.grace_started_at, STOP_HOLD_CAP_SECONDS);
34
34
  case "started":
35
35
  return !olderThan(row.updated_at, HOLD_CEILING_SECONDS);
36
36
  case "respawning":
package/dist/portfolio.js CHANGED
@@ -2,7 +2,7 @@ import { db } from "./db.js";
2
2
  import { listProjects } from "./context.js";
3
3
  import { existsSync } from "node:fs";
4
4
  import { parseTags } from "./result.js";
5
- import { OPEN_BLOCKERS_SQL } from "./tools/todos.js";
5
+ import { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
6
6
  import { pendingQuestionCount } from "./questions.js";
7
7
  import { leadSessionEnded, readLeadTurnState } from "./leadState.js";
8
8
  import { liveTargets, ownershipLiveness, rowOwnership } from "./tmux.js";
@@ -63,7 +63,7 @@ function projectRow(project, now, snapshot) {
63
63
  .prepare(`SELECT t.id, t.title, t.slug, t.priority, t.status, t.tags, t.updated_at,
64
64
  EXISTS (${OPEN_BLOCKERS_SQL}) AS blocked
65
65
  FROM todos t
66
- WHERE t.project_id = ? AND t.status IN ('open', 'in_progress') AND t.archived_at IS NULL`)
66
+ WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}`)
67
67
  .all(project.id);
68
68
  const todos = { open: 0, in_progress: 0, blocked: 0, blocked_in_progress: 0, high: 0 };
69
69
  const tagged = [];
package/dist/processes.js CHANGED
@@ -38,6 +38,18 @@ export const STOP_REASONS = {
38
38
  leadPaneExited: "lead pane exited",
39
39
  previousLead: "left running by a previous lead",
40
40
  };
41
+ export function renderStart(result) {
42
+ switch (result.status) {
43
+ case "started":
44
+ return result.message;
45
+ case "already-running":
46
+ return `already running${result.location ? ` (${result.location})` : ""}`;
47
+ case "refused":
48
+ return result.message ?? "";
49
+ case "failed":
50
+ return `failed: ${result.message}`;
51
+ }
52
+ }
41
53
  const STOPPABLE_COLUMNS = "id, project_id, name, tmux_target, tmux_socket, pane_pid, created_at";
42
54
  export function runningCommandRows(projectId) {
43
55
  return db
package/dist/spawn.js CHANGED
@@ -1,7 +1,7 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { existsSync } from "node:fs";
3
2
  import { preservedRolloutPath, reapCodexHome } from "./codexHome.js";
4
3
  import { getProject } from "./context.js";
4
+ import { gitOut } from "./git.js";
5
5
  import { dataDir, db } from "./db.js";
6
6
  import { loadProjectYml } from "./projectYml.js";
7
7
  import { applyLayout, claimInitialWindow, createWindow, DEFAULT_LAYOUT, ensureSession, crossServerRefusal, findProcessesWindow, findProjectWindow, isPaneTarget, paneProcessExited, paneWindow, PROCESSES_LAYOUT, processesPaneTitle, processesWindowName, retainOnExitArgs, rowLive, sessionName, setPaneTitle, setRemainOnExit, makeProcessesWindow, tmux, tmuxSocketPath, untrustedTmuxServer, waitForPaneEstablished, windowLayout, windowOwner, windowTitle, rowOwnership, } from "./tmux.js";
@@ -347,34 +347,19 @@ export function renameAgent(agent, newName, projectName) {
347
347
  }
348
348
  }
349
349
  export function branchAt(cwd) {
350
- const git = (args) => {
351
- try {
352
- return execFileSync("git", args, {
353
- cwd,
354
- encoding: "utf8",
355
- stdio: ["ignore", "pipe", "ignore"],
356
- timeout: 2000,
357
- }).trim();
358
- }
359
- catch {
360
- return null;
361
- }
362
- };
363
- const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
350
+ const branch = gitOut(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
364
351
  if (branch === null)
365
352
  return "";
366
353
  if (branch !== "HEAD")
367
354
  return branch;
368
- return git(["rev-parse", "--short", "HEAD"]) ?? "";
355
+ return gitOut(cwd, ["rev-parse", "--short", "HEAD"]) ?? "";
369
356
  }
370
357
  export function parkAgentRow(agentId, expected, branch) {
371
358
  const park = "UPDATE agents SET status = 'closed', closed_at = datetime('now'), parked_at = datetime('now'), " +
372
359
  "parked_branch = ? WHERE id = ? AND status = 'running' AND tmux_target = ?";
373
- const row = typeof expected === "string"
374
- ? db.prepare(`${park} RETURNING parked_at`).get(branch, agentId, expected)
375
- : db
376
- .prepare(`${park} AND tmux_socket = ? AND pane_pid = ? RETURNING parked_at`)
377
- .get(branch, agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
360
+ const row = db
361
+ .prepare(`${park} AND tmux_socket = ? AND pane_pid = ? RETURNING parked_at`)
362
+ .get(branch, agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
378
363
  return row?.parked_at;
379
364
  }
380
365
  export const paneIdentity = (row) => ({
@@ -455,13 +440,9 @@ export function closeAgentRow(agentId, expected) {
455
440
  ? db
456
441
  .prepare("UPDATE agents SET status = 'closed', closed_at = datetime('now') WHERE id = ? AND status = 'running'")
457
442
  .run(agentId)
458
- : typeof expected === "string"
459
- ? db
460
- .prepare("UPDATE agents SET status = 'closed', closed_at = datetime('now') WHERE id = ? AND status = 'running' AND tmux_target = ?")
461
- .run(agentId, expected)
462
- : db
463
- .prepare("UPDATE agents SET status = 'closed', closed_at = datetime('now') WHERE id = ? AND status = 'running' " +
464
- "AND tmux_target = ? AND tmux_socket = ? AND pane_pid = ?")
465
- .run(agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
443
+ : db
444
+ .prepare("UPDATE agents SET status = 'closed', closed_at = datetime('now') WHERE id = ? AND status = 'running' " +
445
+ "AND tmux_target = ? AND tmux_socket = ? AND pane_pid = ?")
446
+ .run(agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
466
447
  return info.changes > 0;
467
448
  }
@@ -0,0 +1,5 @@
1
+ export const ACTIVE_TODO_SQL = "t.status IN ('open', 'in_progress') AND t.archived_at IS NULL";
2
+ export const NOT_COMPLETED_TODO_SQL = "status != 'completed'";
3
+ export const OPEN_BLOCKERS_FROM_WHERE = `FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
4
+ WHERE b.todo_id = t.id AND bt.${NOT_COMPLETED_TODO_SQL}`;
5
+ export const OPEN_BLOCKERS_SQL = `SELECT 1 ${OPEN_BLOCKERS_FROM_WHERE}`;
@@ -695,19 +695,15 @@ export function registerAgents(server) {
695
695
  note: "The pane never became ready. The brief is loaded regardless; check agent_output before sending the worker its assignment - typing into it now risks losing the text silently.",
696
696
  }),
697
697
  }
698
- : harness.classifiesPaneScreen
699
- ? {
700
- instructions: workerBrief(briefFor(actorId)),
701
- }
702
- : {
703
- brief_path: writeAgentBrief(agentId, workerBrief(briefFor(actorId))),
704
- note: `This worker runs ${JSON.stringify(commandHead(baseCommand))}, which hive cannot brief and ` +
705
- "cannot type into: the guards that make typing safe read claude's chrome, so agent_send's " +
706
- "text path refuses this pane. There are no instructions to prepend, because there is no " +
707
- "supported way to send them. The brief is written to brief_path - open that pane " +
708
- "(tmux attach) and paste it in yourself, or drive the worker with agent_send(keys: [...]). " +
709
- "Wakes aimed at this worker are refused for the same reason.",
710
- }),
698
+ : {
699
+ brief_path: writeAgentBrief(agentId, workerBrief(briefFor(actorId))),
700
+ note: `This worker runs ${JSON.stringify(commandHead(baseCommand))}, which hive cannot brief and ` +
701
+ "cannot type into: the guards that make typing safe read claude's chrome, so agent_send's " +
702
+ "text path refuses this pane. There are no instructions to prepend, because there is no " +
703
+ "supported way to send them. The brief is written to brief_path - open that pane " +
704
+ "(tmux attach) and paste it in yourself, or drive the worker with agent_send(keys: [...]). " +
705
+ "Wakes aimed at this worker are refused for the same reason.",
706
+ }),
711
707
  };
712
708
  if (omittedTodoWarning) {
713
709
  const existingNote = typeof receipt.note === "string" ? receipt.note : "";
@@ -6,6 +6,7 @@ import { matchesAnyTag, parseTags, run } from "../result.js";
6
6
  import { findUnsafeControlChar } from "../tmux.js";
7
7
  import { SLUG_MAX_LEN, fallbackSlug } from "../slug.js";
8
8
  import { idParam, limitParam, offsetParam, projectIdParam } from "./params.js";
9
+ import { NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_FROM_WHERE, OPEN_BLOCKERS_SQL } from "../todoScope.js";
9
10
  const priorityParam = z.enum(["high", "medium", "low"]);
10
11
  const slugParam = z
11
12
  .string()
@@ -19,11 +20,8 @@ const slugParam = z
19
20
  export { SLUG_MAX_LEN, fallbackSlug };
20
21
  export const TODO_STATUSES = ["open", "in_progress", "backlog", "completed"];
21
22
  const statusParam = z.enum(TODO_STATUSES);
22
- const OPEN_BLOCKERS_FROM_WHERE = `FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
23
- WHERE b.todo_id = t.id AND bt.status != 'completed'`;
24
- export const OPEN_BLOCKERS_SQL = `SELECT 1 ${OPEN_BLOCKERS_FROM_WHERE}`;
25
23
  const LIVE_DEPENDENTS_SQL = `SELECT t.id AS todo_id FROM todo_blockers b JOIN todos t ON t.id = b.todo_id
26
- WHERE b.blocker_id = ? AND t.status != 'completed' AND t.archived_at IS NULL`;
24
+ WHERE b.blocker_id = ? AND t.${NOT_COMPLETED_TODO_SQL} AND t.archived_at IS NULL`;
27
25
  export const COMMENT_COUNT_SQL = `(SELECT COUNT(*) FROM todo_comments c WHERE c.todo_id = t.id)`;
28
26
  const SUMMARY_SQL = `
29
27
  SELECT t.*,
@@ -191,10 +189,7 @@ const completeTodo = db.transaction((projectId, todoId, completed) => {
191
189
  if (completed) {
192
190
  newlyUnblocked = db
193
191
  .prepare(`${LIVE_DEPENDENTS_SQL}
194
- AND NOT EXISTS (
195
- SELECT 1 FROM todo_blockers b2 JOIN todos bt ON bt.id = b2.blocker_id
196
- WHERE b2.todo_id = t.id AND bt.status != 'completed'
197
- )`)
192
+ AND NOT EXISTS (${OPEN_BLOCKERS_SQL})`)
198
193
  .all(todo.id).map((r) => r.todo_id);
199
194
  }
200
195
  return { todo_id: todo.id, completed, newly_unblocked: newlyUnblocked };
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { readFileSync, statSync } from "node:fs";
3
2
  import { dirname, join } from "node:path";
4
3
  import { fileURLToPath } from "node:url";
4
+ import { gitOut } from "./git.js";
5
5
  const moduleDir = dirname(fileURLToPath(import.meta.url));
6
6
  const checkoutRoot = join(moduleDir, "..");
7
7
  function readBuildInfo() {
@@ -60,22 +60,9 @@ function packageVersion() {
60
60
  }
61
61
  }
62
62
  function currentGit() {
63
- try {
64
- const sha = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
65
- cwd: checkoutRoot,
66
- encoding: "utf8",
67
- stdio: ["ignore", "pipe", "ignore"],
68
- }).trim();
69
- const status = execFileSync("git", ["status", "--porcelain"], {
70
- cwd: checkoutRoot,
71
- encoding: "utf8",
72
- stdio: ["ignore", "pipe", "ignore"],
73
- });
74
- return { sha, dirty: status.trim().length > 0 };
75
- }
76
- catch {
77
- return null;
78
- }
63
+ const sha = gitOut(checkoutRoot, ["rev-parse", "--short", "HEAD"]);
64
+ const status = gitOut(checkoutRoot, ["status", "--porcelain"]);
65
+ return sha === null || status === null ? null : { sha, dirty: status.length > 0 };
79
66
  }
80
67
  const describe = (sha, dirty) => `${sha}${dirty ? "-dirty" : ""}`;
81
68
  export function versionInfo() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmgmyr/hive",
3
- "version": "1.11.4",
3
+ "version": "1.11.5",
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": {