@cmgmyr/hive 1.9.1 → 1.10.0

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.
@@ -2,6 +2,7 @@ import { existsSync, realpathSync } from "node:fs";
2
2
  import { join, sep } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { db } from "./db.js";
5
+ import { SECTION_STYLE, SPRITE, chevron, renderBoardPadsTodos, sectionScript } from "./dashboard.js";
5
6
  export const QUEEN_BRIEF_KEY = "queen:brief";
6
7
  const LANE_ORDER = ["waiting_on_you", "stuck", "moving", "quiet"];
7
8
  const LANE_RANK = { waiting_on_you: 0, stuck: 1, moving: 2, quiet: 3 };
@@ -158,6 +159,8 @@ function leadPill(p) {
158
159
  return `<span class="status status-ok">lead up</span>${turnPill(p)}`;
159
160
  case "dead_pane":
160
161
  return '<span class="status status-fail">lead dead</span>';
162
+ case "dormant":
163
+ return '<span class="status status-off">lead dormant</span>';
161
164
  case "unknown":
162
165
  return '<span class="status status-off">lead unknown</span>';
163
166
  default:
@@ -243,7 +246,7 @@ function quietList(list, asOf, href) {
243
246
  return ('<section class="card"><ul class="quiet-list">' +
244
247
  list
245
248
  .map((p) => {
246
- const sub = `${p.lead.state === "alive" ? (turnText(p) ? `lead up, ${turnText(p)}` : "lead up") : p.lead.state === "dead_pane" ? "lead dead" : p.lead.state === "unknown" ? "lead unknown" : "no lead"}, ${p.todos.open} open`;
249
+ const sub = `${p.lead.state === "alive" ? (turnText(p) ? `lead up, ${turnText(p)}` : "lead up") : p.lead.state === "dead_pane" ? "lead dead" : p.lead.state === "dormant" ? "lead dormant" : p.lead.state === "unknown" ? "lead unknown" : "no lead"}, ${p.todos.open} open`;
247
250
  return (`<li data-project="${p.id}"><span>${projectLink(p, href)}` +
248
251
  `<a class="sub" href="#row-${p.id}" data-jump="${p.id}">${esc(sub)}</a></span>` +
249
252
  `<span class="age">${esc(age(p.last_activity_at, asOf))}</span></li>`);
@@ -338,7 +341,7 @@ function briefSection(state, report, href, queenHomeId) {
338
341
  (picks ? `<ol>${picks}</ol>` : "") +
339
342
  `${driftLine(brief, report, queenHomeId)}</section>`);
340
343
  }
341
- const CSS = `
344
+ const CSS = `${SECTION_STYLE}
342
345
  :root { color-scheme: light;
343
346
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
344
347
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
@@ -350,7 +353,7 @@ const CSS = `
350
353
  --accent: #1f5fd0; --focus: #1f5fd0; --hover: #f1f4f8;
351
354
  --shadow: 0 1px 2px rgba(19, 26, 38, 0.06), 0 2px 8px rgba(19, 26, 38, 0.05);
352
355
  --card-border: transparent; --r-card: 14px; --r-ctl: 9px; --r-pill: 999px;
353
- --ease-out: cubic-bezier(0.23, 1, 0.32, 1); }
356
+ --ease-out: cubic-bezier(0.23, 1, 0.32, 1); --header-offset: 5rem; }
354
357
  @media (prefers-color-scheme: dark) {
355
358
  :root:not([data-theme="light"]) { color-scheme: dark;
356
359
  --bg: #101317; --panel: #171b21; --panel-sunken: #1c2129;
@@ -478,17 +481,22 @@ a.proj:hover { color: var(--accent); text-decoration: underline; text-underline-
478
481
  a.sub:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
479
482
  .turn { font-size: 0.75rem; color: var(--fg-muted); }
480
483
  .turn-work { color: var(--live); }
481
- .c-audit { margin-top: 1.5rem; padding-bottom: 0.85rem; }
484
+ .c-audit { margin-top: 1.5rem; }
485
+ .c-audit[open] { padding-bottom: 0.85rem; }
486
+ details.card:not([open]) > .card-head { padding-bottom: 0.9rem; }
482
487
  .c-audit .audit-list, .c-audit .brief-empty { padding: 0 1.1rem; }
483
488
  .audit-list { list-style: none; margin: 0.5rem 0 0; padding: 0; display: grid; gap: 0.4rem; font-size: 0.8125rem; }
484
489
  .audit-list li { display: flex; flex-wrap: wrap; gap: 0.15rem 0.75rem; align-items: baseline; }
485
490
  .audit-op { font-weight: 600; }
486
491
  .audit-sum { color: var(--fg-muted); min-width: 0; overflow-wrap: anywhere; }
487
492
  .d-grid { margin-top: 1.5rem; }
493
+ .card-head > .chev { margin-left: auto; }
494
+ .home { margin-top: 1.5rem; }
495
+ .home-head { padding: 0 0.2rem 0.6rem; }
488
496
  .d-grid .g-row:not(.g-head) { box-shadow: inset 3px 0 0 var(--lane, transparent); }
489
497
  .d-grid .g-row { scroll-margin-top: 6rem; transition: background-color 600ms ease; }
490
498
  .d-grid .g-row.is-flash { background: var(--live-bg); transition: none; }
491
- .empty { margin-top: 1.25rem; padding: 1.25rem 1.1rem; color: var(--fg-muted); }
499
+ .no-projects { margin-top: 1.25rem; padding: 1.25rem 1.1rem; color: var(--fg-muted); }
492
500
  @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
493
501
  `;
494
502
  const SCRIPT = `(function () {
@@ -504,12 +512,17 @@ const SCRIPT = `(function () {
504
512
  var row = document.getElementById("row-" + j.getAttribute("data-jump"));
505
513
  if (!row) return;
506
514
  e.preventDefault();
515
+ var box = row.closest("details");
516
+ if (box) box.open = true;
507
517
  var still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
508
518
  row.scrollIntoView({ behavior: still ? "auto" : "smooth", block: "center" });
509
519
  row.classList.add("is-flash");
510
520
  setTimeout(function () { row.classList.remove("is-flash"); }, 900);
511
521
  });
512
522
  })();`;
523
+ const SECTIONS_SCRIPT = `(function () {
524
+ ${sectionScript("hive-queen-state")}
525
+ })();`;
513
526
  const MARK = '<symbol id="mark" viewBox="0 0 32 32"><path d="M16 3.2 27 9.6v12.8L16 28.8 5 22.4V9.6Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M16 11 21 14v6l-5 3-5-3v-6Z" fill="currentColor" fill-opacity="0.22" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></symbol>';
514
527
  export const QUEEN_GENERATED_MARKER = "data-generated";
515
528
  function auditSection(audit, report) {
@@ -525,7 +538,7 @@ function auditSection(audit, report) {
525
538
  .join("") +
526
539
  "</ul>"
527
540
  : '<p class="brief-empty">No queen actions recorded yet.</p>';
528
- return `<section class="card c-audit"><div class="card-head"><h2>Recent queen actions</h2><span class="card-sub">the newest writes the queen made into other projects</span></div>${body}</section>`;
541
+ return `<details class="card c-audit" id="queen-recent-actions" open><summary class="card-head"><h2>Recent queen actions</h2><span class="card-sub">the newest writes the queen made into other projects</span>${chevron()}</summary>${body}</details>`;
529
542
  }
530
543
  export function renderQueenDashboard(report, briefState, href = (p) => dashboardHref(p.root), audit = [], queenHomeId = null) {
531
544
  const asOf = report.as_of;
@@ -552,22 +565,26 @@ export function renderQueenDashboard(report, briefState, href = (p) => dashboard
552
565
  `<span class="count">${report.totals.lanes[lane]}</span></h2></div>` +
553
566
  `<p class="lane-rule">${LANE_RULE[lane]}</p>${body}</section>`);
554
567
  }).join("");
568
+ const home = queenHomeId === null
569
+ ? ""
570
+ : `<div class="home"><div class="home-head"><h2>Queen home project</h2></div>${renderBoardPadsTodos(queenHomeId)}</div>`;
555
571
  const main = n === 0
556
- ? `${briefSection(briefState, report, href, queenHomeId)}<section class="card empty"><h2>No projects registered</h2><p>Run <code>hive lead</code> in a project folder to register it, and it will appear here.</p></section>`
572
+ ? `${briefSection(briefState, report, href, queenHomeId)}<section class="card no-projects"><h2>No projects registered</h2><p>Run <code>hive lead</code> in a project folder to register it, and it will appear here.</p></section>`
557
573
  : `${briefSection(briefState, report, href, queenHomeId)}<div class="lanes">${lanes}</div>` +
558
- `<section class="card d-grid"><div class="card-head"><h2>Every project <span class="count">${n}</span></h2>` +
559
- '<span class="card-sub">in lane order; a name opens that project’s dashboard</span></div>' +
574
+ `<details class="card d-grid" id="queen-every-project" open><summary class="card-head"><h2>Every project <span class="count">${n}</span></h2>` +
575
+ `<span class="card-sub">in lane order; a name opens that project’s dashboard</span>${chevron()}</summary>` +
560
576
  `<div class="grid" role="table">${GRID_HEAD}${ordered.map((p) => gridRow(p, asOf, href)).join("")}</div>` +
561
- '<p class="freshnote">Every row here is read from the store when the page is written. Only the picks are written by a model, which is why they carry a time.</p></section>' +
562
- auditSection(audit, report);
577
+ '<p class="freshnote">Every row here is read from the store when the page is written. Only the picks are written by a model, which is why they carry a time.</p></details>';
578
+ const page = main + home + (n === 0 ? "" : auditSection(audit, report));
563
579
  return ('<!doctype html><html lang="en"><head><meta charset="utf-8">' +
564
580
  '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
565
581
  `<title>All projects</title><style>${CSS}</style></head><body>` +
582
+ SPRITE +
566
583
  `<svg style="position:absolute;width:0;height:0" aria-hidden="true"><defs>${MARK}</defs></svg>` +
567
- '<header class="topbar"><div class="wrap topbar-inner"><div class="brand">' +
584
+ '<header class="topbar" id="topbar"><div class="wrap topbar-inner"><div class="brand">' +
568
585
  '<svg class="mark" role="img" aria-label="hive"><use href="#mark"/></svg><div><h1>All projects</h1>' +
569
586
  `<p class="generated" ${QUEEN_GENERATED_MARKER}>Live rows as of ${esc(asOf)} UTC. ${n} project${n === 1 ? "" : "s"} on this machine.</p></div></div>` +
570
587
  '<label class="live-toggle" title="Off stops this page reloading. The scheduler still rewrites the file.">' +
571
588
  '<input type="checkbox" id="autoreload"> Reload every minute</label></div></header>' +
572
- `<main class="wrap">${main}</main><script>${SCRIPT}</script></body></html>\n`);
589
+ `<main class="wrap">${page}</main><script>${SCRIPT}\n${SECTIONS_SCRIPT}</script></body></html>\n`);
573
590
  }
package/dist/spawn.js CHANGED
@@ -171,11 +171,11 @@ export function launchAgent(spec) {
171
171
  let info;
172
172
  try {
173
173
  info = db
174
- .prepare("INSERT INTO agents (project_id, name, command, cwd, kind, parent_actor_id, tmux_socket, session_id, codex_home, model, extra_args) " +
175
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
174
+ .prepare("INSERT INTO agents (project_id, name, command, cwd, kind, parent_actor_id, tmux_socket, session_id, codex_home, model, extra_args, todo_id) " +
175
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
176
176
  .run(spec.projectId, spec.name, typeof spec.commandString === "string" ? spec.commandString : "", spec.cwd, spec.kind, spec.parentActor, socket, spec.sessionId ?? "", spec.codexHome ?? "", spec.model ?? null, spec.readOnly
177
177
  ? JSON.stringify({ args: spec.extraArgs ?? [], read_only: true })
178
- : spec.extraArgs ? JSON.stringify(spec.extraArgs) : null);
178
+ : spec.extraArgs ? JSON.stringify(spec.extraArgs) : null, spec.todoId ?? null);
179
179
  }
180
180
  catch (e) {
181
181
  throw asNameClash(e, spec.name);
@@ -17,6 +17,7 @@ import { markGoneReported } from "../scheduler.js";
17
17
  import { agentIdentityEnv, branchAt, closeAgentRow, currentOwnership, discardOrphanedPane, isReservedAgentName, isRunningLeadActor, killAgentPane, launchAgent, LEAD_KIND, PaneOwnershipLost, paneIdentity, parkAgentRow, reapCodexHomeForClosedAgent, requireStillOwned, releaseParkRow, renameAgent, resumeAgent, storedLaunchOptions, } from "../spawn.js";
18
18
  import { COMMAND_KIND, runningCommandRow, stopLine, stopProcess, STOP_REASONS } from "../processes.js";
19
19
  import { readContextTokens, readContextFill, resolveTranscriptDir } from "../transcript.js";
20
+ import { leadSessionEnded } from "../leadState.js";
20
21
  import { capturePane, captureFinalScreen, DEFAULT_LAYOUT, cancelCopyMode, describePaneChoice, ensureAttached, findUnsafeControlChar, holdsHumanInput, liveTargets, observationFailed, observeRowOwnership, ownershipLiveness, paneCurrentCommand, paneInCopyMode, paneWindow, pollPaneReadiness, rowOwnership, sendText, sessionName, setRemainOnExit, sleep, TEXT_ALLOWED_CONTROL_CHARS, tmux, TmuxTimeoutError, WINDOW_LAYOUTS, } from "../tmux.js";
21
22
  import { agentIdParam, agentNameParam, idParam, projectIdParam } from "./params.js";
22
23
  import { deriveProvenance, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "../stateProvenance.js";
@@ -357,7 +358,7 @@ function paneField(row, alive) {
357
358
  return { pane: describePaneChoice(awaitingChoice) };
358
359
  }
359
360
  function agentSummary(row, snapshot) {
360
- const alive = summaryLiveness(row, snapshot);
361
+ const { alive, dormant } = row.kind === LEAD_KIND ? leadSummaryLiveness(row, snapshot) : { alive: summaryLiveness(row, snapshot), dormant: null };
361
362
  const { state, ...provenance } = deriveProvenance(row, alive);
362
363
  return {
363
364
  agent_id: row.id,
@@ -378,7 +379,9 @@ function agentSummary(row, snapshot) {
378
379
  command: row.command,
379
380
  cwd: row.cwd,
380
381
  parent_actor_id: row.parent_actor_id,
382
+ todo_id: row.todo_id,
381
383
  created_at: row.created_at,
384
+ ...(dormant ? { dormant_since: dormant.ended_at } : {}),
382
385
  };
383
386
  }
384
387
  export function contextCheckpointEnv(config) {
@@ -419,6 +422,9 @@ export function registerAgents(server) {
419
422
  .describe("Spawn a known harness by name (e.g. \"codex\") instead of a raw command. Ignored when command is also given. Must be in the project's hive.yml agents: list (default: claude only)."),
420
423
  extra_args: z.array(z.string()).optional().describe("Extra CLI arguments."),
421
424
  read_only: z.boolean().optional().describe("Prevent local file writes and mutating shell commands while allowing Hive MCP tools. Only bare claude/codex executables. Extra args allow codex -c model_reasoning_effort=low|medium|high|xhigh, or claude --effort low|medium|high|xhigh|max; all other arguments are refused."),
425
+ todo_id: idParam
426
+ .optional()
427
+ .describe("The todo this worker is on, recorded as a link (a non-archived todo in the same project). Changes no todo status."),
422
428
  cwd: z
423
429
  .string()
424
430
  .optional()
@@ -468,6 +474,20 @@ export function registerAgents(server) {
468
474
  throw new Error(`cwd ${cwd} belongs to project "${cwdProject.name}" (id ${cwdProject.id}), but this spawn resolved to project "${project.name}" (id ${project.id}). ${remedy}`);
469
475
  }
470
476
  }
477
+ if (args.todo_id !== undefined) {
478
+ const linked = db
479
+ .prepare("SELECT archived_at FROM todos WHERE id = ? AND project_id = ?")
480
+ .get(args.todo_id, project.id);
481
+ if (!linked)
482
+ throw new Error(`todo_id ${args.todo_id} is not a todo in project "${project.name}" (id ${project.id}).`);
483
+ if (linked.archived_at != null)
484
+ throw new Error(`todo_id ${args.todo_id} is archived; unarchive it or link another todo.`);
485
+ }
486
+ const omittedTodoWarning = args.todo_id === undefined && db
487
+ .prepare("SELECT 1 FROM todos WHERE project_id = ? AND status = 'in_progress' AND archived_at IS NULL LIMIT 1")
488
+ .get(project.id)
489
+ ? "todo_id was omitted while this project has an in-progress todo; the crew view may infer a link from this worker's comments or pad updates."
490
+ : undefined;
471
491
  const name = args.name != null
472
492
  ? normalizeAgentName(args.name, "name")
473
493
  : nextWorkerName(project.id);
@@ -566,6 +586,7 @@ export function registerAgents(server) {
566
586
  model: args.model,
567
587
  extraArgs: args.extra_args,
568
588
  readOnly: args.read_only,
589
+ todoId: args.todo_id,
569
590
  retainOnExit: harness.classifiesPaneScreen,
570
591
  });
571
592
  }
@@ -622,7 +643,7 @@ export function registerAgents(server) {
622
643
  }
623
644
  }
624
645
  const codexInstructions = codexHomeKey ? codexInstructionsPhrase(codexInstructionLayers) : undefined;
625
- return {
646
+ const receipt = {
626
647
  agent_id: agentId,
627
648
  ...(args.read_only ? { read_only: true } : {}),
628
649
  actor_id: actorId,
@@ -671,6 +692,11 @@ export function registerAgents(server) {
671
692
  "Wakes aimed at this worker are refused for the same reason.",
672
693
  }),
673
694
  };
695
+ if (omittedTodoWarning) {
696
+ const existingNote = typeof receipt.note === "string" ? receipt.note : "";
697
+ receipt.note = existingNote ? `${existingNote} ${omittedTodoWarning}` : omittedTodoWarning;
698
+ }
699
+ return receipt;
674
700
  }));
675
701
  server.registerTool("agent_resume", {
676
702
  description: "Resume a CLOSED claude or codex worker from its recorded session id (claude --resume / codex resume): a fresh pane, the same actor_id, and the worker's full prior context. Addressed by name or agent_id among closed agents (agent_list(include_closed: true)). Send it its next instruction with agent_send once resumed - this tool does not.",
@@ -1580,3 +1606,8 @@ function readInputBox(classifier, target) {
1580
1606
  const box = classifier.inputBoxState(target);
1581
1607
  return box === null ? null : { box };
1582
1608
  }
1609
+ function leadSummaryLiveness(row, snapshot) {
1610
+ const ownership = row.status === "running" ? rowOwnership(row, snapshot) : null;
1611
+ const alive = ownership === null ? false : ownershipLiveness(ownership);
1612
+ return { alive, dormant: ownership === null ? null : leadSessionEnded(row, ownership) };
1613
+ }
@@ -35,6 +35,9 @@ function usageTokens(record) {
35
35
  return tokenNumber(sum) && sum > 0 ? sum : null;
36
36
  }
37
37
  function tailRecords(path) {
38
+ return tailRead(path).records;
39
+ }
40
+ function tailRead(path) {
38
41
  let fd;
39
42
  try {
40
43
  fd = openSync(path, "r");
@@ -54,10 +57,10 @@ function tailRecords(path) {
54
57
  }
55
58
  catch { }
56
59
  }
57
- return records;
60
+ return { records, clipped: start > 0 };
58
61
  }
59
62
  catch {
60
- return [];
63
+ return { records: [], clipped: false };
61
64
  }
62
65
  finally {
63
66
  if (fd !== undefined)
@@ -124,3 +127,133 @@ export function readContextTokens(cwd, sessionId, explicitPath = "") {
124
127
  return null;
125
128
  return claudeTokens(tailRecords(transcriptPath(cwd, sessionId, explicitPath)));
126
129
  }
130
+ export const RECENT_TOOL_CALLS = 8;
131
+ export const SKILL_TOOL = "Skill";
132
+ export function lastComponent(name) {
133
+ return name.split(/__|[.:/]/).filter(Boolean).pop() ?? name;
134
+ }
135
+ function timestamp(record) {
136
+ return typeof record.timestamp === "string" && record.timestamp !== "" ? record.timestamp : null;
137
+ }
138
+ function reviewMarker(texts, at, skills) {
139
+ for (const skill of skills) {
140
+ const invocation = new RegExp(`^\\s*\\$${skill.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "m");
141
+ if (texts.some((text) => typeof text === "string" && invocation.test(text))) {
142
+ return [{ name: SKILL_TOOL, input: { skill }, at }];
143
+ }
144
+ }
145
+ return [];
146
+ }
147
+ function claudeToolCalls(record, options) {
148
+ if (record.type !== "assistant")
149
+ return [];
150
+ const content = object(record.message)?.content;
151
+ if (!Array.isArray(content))
152
+ return [];
153
+ const at = timestamp(record);
154
+ const calls = [];
155
+ for (const part of content) {
156
+ const block = object(part);
157
+ if (!block)
158
+ continue;
159
+ if (block.type === "tool_use" && typeof block.name === "string") {
160
+ calls.push({ name: block.name, input: object(block.input) ?? {}, at });
161
+ }
162
+ else if (block.type === "text") {
163
+ calls.push(...reviewMarker([block.text], at, options.reviewSkills ?? []));
164
+ }
165
+ }
166
+ return calls;
167
+ }
168
+ function stringLiteral(text) {
169
+ const quote = text[0];
170
+ if (quote !== '"' && quote !== "'")
171
+ return null;
172
+ const json = quote === '"' ? text : `"${text.slice(1, -1).replace(/\\'/g, "'").replace(/(\\.)|"/g, (m, esc) => esc ?? '\\"')}"`;
173
+ try {
174
+ const value = JSON.parse(json);
175
+ return typeof value === "string" ? value : null;
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ }
181
+ function execInnerCalls(source, at) {
182
+ const marks = [...source.matchAll(/\btools\.([A-Za-z_]\w*)\s*\(/g)];
183
+ if (marks.length === 0)
184
+ return null;
185
+ const calls = [];
186
+ marks.forEach((mark, i) => {
187
+ const name = lastComponent(mark[1]);
188
+ if (name === "write_stdin")
189
+ return;
190
+ if (name === "exec_command") {
191
+ const span = source.slice(mark.index + mark[0].length, marks[i + 1]?.index ?? source.length);
192
+ const cmd = /["']?\bcmd["']?\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/.exec(span);
193
+ const command = cmd ? stringLiteral(cmd[1]) : null;
194
+ if (command !== null)
195
+ calls.push({ name: "Bash", input: { command }, at });
196
+ return;
197
+ }
198
+ calls.push({ name, input: {}, at });
199
+ });
200
+ return calls;
201
+ }
202
+ function codexToolCalls(record, options) {
203
+ const payload = object(record.payload);
204
+ if (record.type !== "response_item" || !payload)
205
+ return [];
206
+ const at = timestamp(record);
207
+ if (payload.type === "message") {
208
+ if (payload.role !== "assistant" || !Array.isArray(payload.content))
209
+ return [];
210
+ return reviewMarker(payload.content.map((part) => object(part)?.text), at, options.reviewSkills ?? []);
211
+ }
212
+ if (typeof payload.name !== "string")
213
+ return [];
214
+ const name = lastComponent(payload.name);
215
+ if (payload.type === "custom_tool_call") {
216
+ if (name === "exec" && typeof payload.input === "string") {
217
+ const inner = execInnerCalls(payload.input, at);
218
+ if (inner)
219
+ return inner;
220
+ }
221
+ return [{ name, input: typeof payload.input === "string" ? { input: payload.input } : {}, at }];
222
+ }
223
+ if (payload.type !== "function_call")
224
+ return [];
225
+ if (name === "write_stdin" || name === "sleep")
226
+ return [];
227
+ let args = {};
228
+ if (typeof payload.arguments === "string") {
229
+ try {
230
+ args = object(JSON.parse(payload.arguments)) ?? {};
231
+ }
232
+ catch { }
233
+ }
234
+ if (name === "exec_command")
235
+ return typeof args.cmd === "string" ? [{ name: "Bash", input: { command: args.cmd }, at }] : [];
236
+ return [{ name, input: args, at }];
237
+ }
238
+ export function readRecentToolCalls(kind, worker, options = {}) {
239
+ const path = worker.transcript_path || (kind === "claude" && worker.session_id
240
+ ? join(transcriptDir(worker.cwd), `${worker.session_id}.jsonl`) : "");
241
+ if (!path)
242
+ return { calls: [], truncated: false };
243
+ const { records, clipped } = tailRead(path);
244
+ const newestFirst = [];
245
+ let capped = false;
246
+ for (const record of records) {
247
+ const calls = (kind === "claude" ? claudeToolCalls : codexToolCalls)(record, options);
248
+ for (let i = calls.length - 1; i >= 0; i--) {
249
+ if (newestFirst.length === RECENT_TOOL_CALLS) {
250
+ capped = true;
251
+ break;
252
+ }
253
+ newestFirst.push(calls[i]);
254
+ }
255
+ if (capped)
256
+ break;
257
+ }
258
+ return { calls: newestFirst.reverse(), truncated: clipped || capped };
259
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmgmyr/hive",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
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": {