@cmgmyr/hive 1.11.4 → 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.
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)
@@ -26,9 +17,9 @@ export function truncate(text, limit) {
26
17
  async function digest(projectPath, profile, warnings) {
27
18
  const { migrate, db } = await import("./db.js");
28
19
  const { findProjectForCwd } = await import("./context.js");
29
- const { ACTIVE_TIMER_WHERE } = await import("./scheduler.js");
30
- const { getActivePadByName } = await import("./tools/pads.js");
31
- const { OPEN_BLOCKERS_SQL } = await import("./tools/todos.js");
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");
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":
@@ -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
@@ -2,8 +2,10 @@ 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
+ 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"}`
@@ -63,7 +64,7 @@ function projectRow(project, now, snapshot) {
63
64
  .prepare(`SELECT t.id, t.title, t.slug, t.priority, t.status, t.tags, t.updated_at,
64
65
  EXISTS (${OPEN_BLOCKERS_SQL}) AS blocked
65
66
  FROM todos t
66
- WHERE t.project_id = ? AND t.status IN ('open', 'in_progress') AND t.archived_at IS NULL`)
67
+ WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}`)
67
68
  .all(project.id);
68
69
  const todos = { open: 0, in_progress: 0, blocked: 0, blocked_in_progress: 0, high: 0 };
69
70
  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
@@ -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,7 +1,8 @@
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 { LEAD_KIND, LEAD_NAME } from "./agentKinds.js";
5
+ import { gitOut } from "./git.js";
5
6
  import { dataDir, db } from "./db.js";
6
7
  import { loadProjectYml } from "./projectYml.js";
7
8
  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";
@@ -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);
@@ -347,34 +343,19 @@ export function renameAgent(agent, newName, projectName) {
347
343
  }
348
344
  }
349
345
  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"]);
346
+ const branch = gitOut(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
364
347
  if (branch === null)
365
348
  return "";
366
349
  if (branch !== "HEAD")
367
350
  return branch;
368
- return git(["rev-parse", "--short", "HEAD"]) ?? "";
351
+ return gitOut(cwd, ["rev-parse", "--short", "HEAD"]) ?? "";
369
352
  }
370
353
  export function parkAgentRow(agentId, expected, branch) {
371
354
  const park = "UPDATE agents SET status = 'closed', closed_at = datetime('now'), parked_at = datetime('now'), " +
372
355
  "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);
356
+ const row = db
357
+ .prepare(`${park} AND tmux_socket = ? AND pane_pid = ? RETURNING parked_at`)
358
+ .get(branch, agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
378
359
  return row?.parked_at;
379
360
  }
380
361
  export const paneIdentity = (row) => ({
@@ -455,13 +436,9 @@ export function closeAgentRow(agentId, expected) {
455
436
  ? db
456
437
  .prepare("UPDATE agents SET status = 'closed', closed_at = datetime('now') WHERE id = ? AND status = 'running'")
457
438
  .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);
439
+ : db
440
+ .prepare("UPDATE agents SET status = 'closed', closed_at = datetime('now') WHERE id = ? AND status = 'running' " +
441
+ "AND tmux_target = ? AND tmux_socket = ? AND pane_pid = ?")
442
+ .run(agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
466
443
  return info.changes > 0;
467
444
  }
@@ -0,0 +1,6 @@
1
+ export const TODO_STATUSES = ["open", "in_progress", "backlog", "completed"];
2
+ export const ACTIVE_TODO_SQL = "t.status IN ('open', 'in_progress') AND t.archived_at IS NULL";
3
+ export const NOT_COMPLETED_TODO_SQL = "status != 'completed'";
4
+ export const OPEN_BLOCKERS_FROM_WHERE = `FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
5
+ WHERE b.todo_id = t.id AND bt.${NOT_COMPLETED_TODO_SQL}`;
6
+ export const OPEN_BLOCKERS_SQL = `SELECT 1 ${OPEN_BLOCKERS_FROM_WHERE}`;
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
+ }