@cmgmyr/hive 1.2.2 → 1.4.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.
Files changed (48) hide show
  1. package/README.md +6 -1
  2. package/claude-plugin/README.md +4 -1
  3. package/claude-plugin/kickoff.mjs +2 -1
  4. package/claude-plugin/skills/profile/SKILL.md +78 -0
  5. package/claude-plugin/skills/profile/references/interview.md +139 -0
  6. package/claude-plugin/skills/profile/references/recipes/index.md +12 -0
  7. package/claude-plugin/skills/profile/references/recipes/review-decision.md +41 -0
  8. package/claude-plugin/skills/profile/references/recipes/session-continuity.md +37 -0
  9. package/claude-plugin/skills/profile/references/recipes/shared-test-resource.md +45 -0
  10. package/claude-plugin/skills/profile/references/recipes/verification.md +50 -0
  11. package/claude-plugin/skills/profile/references/recipes/worker-context-checkpoint.md +51 -0
  12. package/dist/brief.js +1 -1
  13. package/dist/build-info.json +1 -1
  14. package/dist/cli.js +436 -112
  15. package/dist/codexHome.js +23 -8
  16. package/dist/config.js +8 -61
  17. package/dist/context.js +191 -2
  18. package/dist/db.js +80 -20
  19. package/dist/firstMessage.js +5 -0
  20. package/dist/globalConfig.js +247 -0
  21. package/dist/harnesses.js +14 -4
  22. package/dist/help.js +29 -11
  23. package/dist/hook.js +9 -0
  24. package/dist/hooks.js +2 -1
  25. package/dist/index.js +11 -5
  26. package/dist/kickoff.js +42 -29
  27. package/dist/leadState.js +63 -0
  28. package/dist/leadWatch.js +73 -0
  29. package/dist/portfolio.js +210 -0
  30. package/dist/projectYml.js +242 -178
  31. package/dist/queenAudit.js +118 -0
  32. package/dist/queenDashboard.js +573 -0
  33. package/dist/scheduler.js +190 -20
  34. package/dist/spawn.js +17 -4
  35. package/dist/strictInput.js +24 -7
  36. package/dist/tmux.js +65 -0
  37. package/dist/tools/agents.js +153 -15
  38. package/dist/tools/kv.js +24 -0
  39. package/dist/tools/leases.js +14 -2
  40. package/dist/tools/meta.js +65 -3
  41. package/dist/tools/pads.js +42 -0
  42. package/dist/tools/queenAudit.js +13 -0
  43. package/dist/tools/todos.js +72 -13
  44. package/dist/tools/wakes.js +233 -65
  45. package/package.json +4 -4
  46. package/profiles/orchestration/worker.md +1 -1
  47. package/profiles/queen/posture.md +31 -0
  48. package/profiles/queen/runbook.md +26 -0
package/dist/scheduler.js CHANGED
@@ -1,21 +1,26 @@
1
+ import { QUEEN_AUDIT_RETENTION, QUEEN_AUDIT_MAX_ROWS, recentQueenAudit } from "./queenAudit.js";
1
2
  import { runningBuildChange, runningBuildNotice } from "./version.js";
2
- import { existsSync, mkdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { createHash } from "node:crypto";
3
5
  import { dirname, join, sep } from "node:path";
4
6
  import { dataDir, db, storeReplaced } from "./db.js";
5
7
  import { maybeBackupHourly } from "./backup.js";
6
8
  import { renderDashboardForWrite } from "./dashboard.js";
7
9
  import { snapshotProcesses } from "./processes.js";
8
10
  import { loadProjectYml } from "./projectYml.js";
9
- import { listProjects } from "./context.js";
11
+ import { getProjectByPath, listProjects, queenHomeDir } from "./context.js";
12
+ import { collectPortfolio } from "./portfolio.js";
13
+ import { QUEEN_GENERATED_MARKER, readQueenBrief, renderQueenDashboard } from "./queenDashboard.js";
10
14
  import { COMMAND_KIND, stoppingMarkerLive } from "./processes.js";
11
15
  import { closeAgentRow, isLeadActorId, LEAD_ACTOR_PREFIX, LEAD_KIND, reapCodexHomeForClosedAgent } from "./spawn.js";
12
16
  import { awaitingFirstPrompt, awaitingFirstPromptSql } from "./firstPrompt.js";
17
+ import { evaluateLeadWatch, leadSubscription } from "./leadWatch.js";
13
18
  import { describeLiveTasks, describeOneTask, liveBackgroundTasks, STALL_BOUND_SECONDS, } from "./backgroundTasks.js";
14
19
  import { MESSAGE_MAX_ROWS, MESSAGE_RETENTION } from "./leadMessage.js";
15
20
  import { harnessFor, hasTranscriptSignal, paneClassifierFor, screenClassifiable, transcriptDirFor } from "./harnesses.js";
16
21
  import { transcriptDir, readContextFill } from "./transcript.js";
17
22
  import { ageSecondsSince, describeLastLogEvent, humanizeAge, lastLogEvent, reportsAgentStateLog, } from "./stateProvenance.js";
18
- import { capturePane, foreignSocket, holdsHumanInput, liveTargets, maskChoiceMarker, paneInCopyMode, paneReissued, rowAlive, rowAliveProbe, rowLiveProbe, sanitizeTail, sendText, tailCaptureLines, tmuxSocketPath, } from "./tmux.js";
23
+ import { capturePane, foreignSocket, holdsHumanInput, liveTargets, maskChoiceMarker, paneInCopyMode, paneReissued, rowAlive, rowAliveProbe, rowLiveProbe, renderTrailerView, sanitizeTail, sendText, tailCaptureLines, sleep, tmuxSocketPath, } from "./tmux.js";
19
24
  import { appendTeardown, NOT_ATTRIBUTED, sqlNow, teardownWindow, TEARDOWN_TRIGGER, } from "./teardown.js";
20
25
  export const WATCH_SCOPE_PROJECT = "project";
21
26
  const isStandingWatch = (timer) => timer.watch_scope === WATCH_SCOPE_PROJECT;
@@ -245,6 +250,19 @@ function cancelTimer(timerId) {
245
250
  export const LOG_RETENTION = "-7 days";
246
251
  const LOG_MAX_ROWS = 20_000;
247
252
  function pruneStateLog() {
253
+ try {
254
+ const staleAudit = stmt("SELECT 1 AS hit FROM queen_audit WHERE created_at < datetime('now', ?) LIMIT 1").get(QUEEN_AUDIT_RETENTION);
255
+ if (staleAudit) {
256
+ stmt("DELETE FROM queen_audit WHERE created_at < datetime('now', ?)").run(QUEEN_AUDIT_RETENTION);
257
+ }
258
+ const hi = stmt("SELECT MAX(id) AS v FROM queen_audit").get().v;
259
+ const lo = stmt("SELECT MIN(id) AS v FROM queen_audit").get().v;
260
+ if (hi != null && lo != null && hi - lo >= QUEEN_AUDIT_MAX_ROWS) {
261
+ stmt("DELETE FROM queen_audit WHERE id <= ?").run(hi - QUEEN_AUDIT_MAX_ROWS);
262
+ }
263
+ }
264
+ catch {
265
+ }
248
266
  try {
249
267
  const stale = stmt("SELECT 1 AS hit FROM agent_state_log WHERE created_at < datetime('now', ?) LIMIT 1").get(LOG_RETENTION);
250
268
  if (stale) {
@@ -339,10 +357,75 @@ function maybeGenerateDashboard(project) {
339
357
  catch {
340
358
  }
341
359
  }
360
+ function queenHome() {
361
+ try {
362
+ const home = queenHomeDir();
363
+ if (lstatSync(home).isSymbolicLink())
364
+ return null;
365
+ const project = getProjectByPath(realpathSync(home));
366
+ return project ? { id: project.id, path: home } : null;
367
+ }
368
+ catch {
369
+ return null;
370
+ }
371
+ }
372
+ function renderQueenPage(queenId) {
373
+ const report = collectPortfolio();
374
+ const html = renderQueenDashboard(report, readQueenBrief(queenId, report.as_of), undefined, recentQueenAudit(10), queenId);
375
+ const stable = html.replace(new RegExp(`<p class="generated" ${QUEEN_GENERATED_MARKER}>[^<]*</p>`), "");
376
+ return { html, hash: createHash("sha256").update(stable).digest("hex") };
377
+ }
378
+ const QUEEN_PAGE_CLAIM_KEY = "hive:queen-page-claim";
379
+ const QUEEN_PAGE_MARK_KEY = "hive:queen-page-mark";
380
+ function claimQueenPageAttempt(queenId) {
381
+ bestEffortRun("INSERT OR IGNORE INTO kv (project_id, key, value, updated_at) VALUES (?, ?, '', '1970-01-01 00:00:00')", queenId, QUEEN_PAGE_CLAIM_KEY);
382
+ return (stmt(`UPDATE kv SET updated_at = datetime('now')
383
+ WHERE project_id = ? AND key = ?
384
+ AND updated_at <= datetime('now', '-${DASHBOARD_MIN_INTERVAL_SECONDS} seconds')`).run(queenId, QUEEN_PAGE_CLAIM_KEY).changes === 1);
385
+ }
386
+ function readQueenPageMark(queenId) {
387
+ const row = stmt("SELECT value FROM kv WHERE project_id = ? AND key = ?").get(queenId, QUEEN_PAGE_MARK_KEY);
388
+ return row?.value ?? null;
389
+ }
390
+ function publishQueenPage(queenId, target, page) {
391
+ writeDashboardAtomically(target, page.html);
392
+ bestEffortRun(`INSERT INTO kv (project_id, key, value) VALUES (?, ?, ?)
393
+ ON CONFLICT(project_id, key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')`, queenId, QUEEN_PAGE_MARK_KEY, page.hash);
394
+ }
395
+ export function generateQueenDashboardNow() {
396
+ try {
397
+ const home = queenHome();
398
+ if (home === null)
399
+ return null;
400
+ const target = join(home.path, "dashboard.html");
401
+ publishQueenPage(home.id, target, renderQueenPage(home.id));
402
+ return target;
403
+ }
404
+ catch {
405
+ return null;
406
+ }
407
+ }
408
+ function maybeGenerateQueenDashboard(queen) {
409
+ try {
410
+ if (!claimQueenPageAttempt(queen.id))
411
+ return;
412
+ const target = join(queen.path, "dashboard.html");
413
+ const page = renderQueenPage(queen.id);
414
+ if (readQueenPageMark(queen.id) === page.hash && existsSync(target))
415
+ return;
416
+ publishQueenPage(queen.id, target, page);
417
+ }
418
+ catch {
419
+ }
420
+ }
342
421
  function maybeGenerateDashboards() {
343
422
  try {
423
+ const queen = queenHome();
344
424
  for (const project of listProjects()) {
345
- maybeGenerateDashboard(project);
425
+ if (queen !== null && project.id === queen.id)
426
+ maybeGenerateQueenDashboard(queen);
427
+ else
428
+ maybeGenerateDashboard(project);
346
429
  }
347
430
  }
348
431
  catch {
@@ -397,8 +480,13 @@ export async function tick(snapshot) {
397
480
  )`).all();
398
481
  const choices = new Map();
399
482
  for (const timer of candidates) {
400
- if (timer.kind === "delay")
483
+ if (timer.kind === "delay") {
401
484
  await fireDelay(timer, snapshot, choices);
485
+ continue;
486
+ }
487
+ const leadWatch = leadSubscription(timer.id);
488
+ if (leadWatch !== null)
489
+ await maybeFireLeadWatch(timer, leadWatch, snapshot, now, choices);
402
490
  else
403
491
  await maybeFireIdle(timer, snapshot, now, choices);
404
492
  }
@@ -1399,6 +1487,41 @@ function deliverable(timer, snapshot, choices) {
1399
1487
  firstHeldAt: timer.held_at != null ? timer.first_held_at : null,
1400
1488
  };
1401
1489
  }
1490
+ const PANE_CLAIM_TTL_SECONDS = 6;
1491
+ const PANE_CLAIM_SETTLE_MS = 500;
1492
+ const PANE_CLAIM_OWNER = "hive:scheduler";
1493
+ function acquirePaneClaim(projectId, key) {
1494
+ try {
1495
+ stmt("INSERT OR IGNORE INTO actors (id, name, kind) VALUES (?, 'hive scheduler', 'scheduler')").run(PANE_CLAIM_OWNER);
1496
+ stmt("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at < datetime('now')").run(projectId, key);
1497
+ const row = stmt(`INSERT INTO leases (project_id, lock_key, owner, expires_at)
1498
+ VALUES (?, ?, ?, datetime('now', printf('+%d seconds', ?)))
1499
+ ON CONFLICT(project_id, lock_key) DO NOTHING
1500
+ RETURNING expires_at`).get(projectId, key, PANE_CLAIM_OWNER, PANE_CLAIM_TTL_SECONDS);
1501
+ return row?.expires_at ?? null;
1502
+ }
1503
+ catch {
1504
+ return null;
1505
+ }
1506
+ }
1507
+ // Serializes check-then-type per pane ACROSS scheduler instances: the per-wake claim is atomic but two
1508
+ // instances claiming different wakes for one pane would otherwise both paste before either submits.
1509
+ // A loser skips this tick and its wake stays unclaimed.
1510
+ async function withPaneClaim(timer, fn) {
1511
+ const key = `wake-pane:${timer.deliver_socket}:${timer.deliver_pane}`;
1512
+ const held = timer.deliver_pane ? acquirePaneClaim(timer.project_id, key) : null;
1513
+ if (held === null)
1514
+ return;
1515
+ try {
1516
+ // Settle: a slow TUI still showing the pasted text would make the next instance hold instead of type.
1517
+ if (await fn())
1518
+ await sleep(PANE_CLAIM_SETTLE_MS);
1519
+ }
1520
+ finally {
1521
+ // Match this acquire's expires_at: after a TTL takeover the owner is shared, so owner alone would delete the taker's row.
1522
+ bestEffortRun("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at = ?", timer.project_id, key, held);
1523
+ }
1524
+ }
1402
1525
  async function fireDelay(timer, snapshot, choices) {
1403
1526
  const disposition = noticeDisposition(timer);
1404
1527
  if (disposition === "aged") {
@@ -1413,9 +1536,12 @@ async function fireDelay(timer, snapshot, choices) {
1413
1536
  bestEffortRun("UPDATE wakes SET cancelled_at = datetime('now') WHERE id = ? AND created_at IS ?", timer.id, timer.created_at);
1414
1537
  return;
1415
1538
  }
1539
+ await withPaneClaim(timer, () => fireDelayClaimed(timer, snapshot, choices));
1540
+ }
1541
+ async function fireDelayClaimed(timer, snapshot, choices) {
1416
1542
  const decision = deliverable(timer, snapshot, choices);
1417
1543
  if (!decision.ok)
1418
- return;
1544
+ return false;
1419
1545
  let claimed;
1420
1546
  if (timer.repeat_every_ms != null) {
1421
1547
  const seconds = Math.max(1, Math.round(timer.repeat_every_ms / 1000));
@@ -1429,8 +1555,10 @@ async function fireDelay(timer, snapshot, choices) {
1429
1555
  else {
1430
1556
  claimed = claimOneShot(timer);
1431
1557
  }
1432
- if (claimed)
1433
- await deliver(timer, "", choices, decision.typedSeen, decision.firstHeldAt);
1558
+ if (!claimed)
1559
+ return false;
1560
+ await deliver(timer, "", choices, decision.typedSeen, decision.firstHeldAt);
1561
+ return true;
1434
1562
  }
1435
1563
  function claimOneShot(timer) {
1436
1564
  return (stmt(`UPDATE wakes SET fired_at = datetime('now'), fire_count = fire_count + 1
@@ -1460,6 +1588,12 @@ function watchedStates(timer, snapshot) {
1460
1588
  };
1461
1589
  });
1462
1590
  }
1591
+ function idleConditionMet(timer, snapshot) {
1592
+ const states = watchedStates(timer, snapshot);
1593
+ return timer.kind === "idle_any"
1594
+ ? states.some((s) => s.gone || (s.idle && s.since != null && s.since >= timer.created_at))
1595
+ : states.length > 0 && states.every((s) => s.idle);
1596
+ }
1463
1597
  async function maybeFireIdle(timer, snapshot, now, choices) {
1464
1598
  const timedOut = timer.max_wait_at != null && timer.max_wait_at <= now;
1465
1599
  if (isStandingWatch(timer)) {
@@ -1468,34 +1602,69 @@ async function maybeFireIdle(timer, snapshot, now, choices) {
1468
1602
  if (snapshot !== null && !timedOut)
1469
1603
  noteBlockedWatched(timer, snapshot, choices);
1470
1604
  if (timedOut) {
1471
- const decision = deliverable(timer, snapshot, choices);
1472
- if (decision.ok && claimOneShot(timer)) {
1605
+ await withPaneClaim(timer, async () => {
1606
+ const decision = deliverable(timer, snapshot, choices);
1607
+ if (!decision.ok || !claimOneShot(timer))
1608
+ return false;
1473
1609
  await deliver(timer, STANDING_EXPIRED_NOTE, choices, decision.typedSeen, decision.firstHeldAt);
1474
- }
1610
+ return true;
1611
+ });
1475
1612
  }
1476
1613
  return;
1477
1614
  }
1478
1615
  let ready = false;
1616
+ let idleMet = false;
1479
1617
  if (timedOut) {
1480
1618
  ready = true;
1619
+ if (snapshot !== null)
1620
+ idleMet = idleConditionMet(timer, snapshot);
1481
1621
  }
1482
1622
  else {
1483
1623
  if (snapshot === null)
1484
1624
  return;
1485
- const states = watchedStates(timer, snapshot);
1486
- ready =
1487
- timer.kind === "idle_any"
1488
- ? states.some((s) => s.gone || (s.idle && s.since != null && s.since >= timer.created_at))
1489
- : states.length > 0 && states.every((s) => s.idle);
1625
+ idleMet = idleConditionMet(timer, snapshot);
1626
+ ready = idleMet;
1490
1627
  if (!ready)
1491
1628
  noteBlockedWatched(timer, snapshot, choices);
1492
1629
  }
1493
1630
  if (ready) {
1631
+ await withPaneClaim(timer, async () => {
1632
+ const decision = deliverable(timer, snapshot, choices);
1633
+ if (!decision.ok || !claimOneShot(timer))
1634
+ return false;
1635
+ await deliver(timer, timedOut && !idleMet ? "max wait reached" : "", choices, decision.typedSeen, decision.firstHeldAt);
1636
+ return true;
1637
+ });
1638
+ }
1639
+ }
1640
+ // A lead watch never enters the worker helpers below: it reads its own subscription row and the
1641
+ // watched lead's own turn record, and ends with a named reason rather than ever reading as idle.
1642
+ async function maybeFireLeadWatch(timer, sub, snapshot, now, choices) {
1643
+ const timedOut = timer.max_wait_at != null && timer.max_wait_at <= now;
1644
+ const readers = {
1645
+ awaitingChoice: (pane, command) => awaitingChoice(pane, command, choices),
1646
+ holdsInput: (pane, command) => inputBoxHoldsWake(pane, command, choices),
1647
+ };
1648
+ const result = evaluateLeadWatch(sub, timer.kind === "idle_all" ? "all" : "any", snapshot, readers);
1649
+ const note = result.kind === "idle"
1650
+ ? "watched lead's turn ended (not necessarily finished; read it before acting)"
1651
+ : result.kind === "invalid"
1652
+ ? `lead watch ended: ${result.reason}`
1653
+ : timedOut
1654
+ ? "max wait reached"
1655
+ : null;
1656
+ if (note === null)
1657
+ return;
1658
+ await withPaneClaim(timer, async () => {
1494
1659
  const decision = deliverable(timer, snapshot, choices);
1495
- if (decision.ok && claimOneShot(timer)) {
1496
- await deliver(timer, timedOut ? "max wait reached" : "", choices, decision.typedSeen, decision.firstHeldAt);
1660
+ if (!decision.ok || !claimOneShot(timer))
1661
+ return false;
1662
+ if (result.kind === "invalid") {
1663
+ bestEffortRun("UPDATE lead_idle_subscriptions SET terminal_reason = ? WHERE wake_id = ?", result.reason, timer.id);
1497
1664
  }
1498
- }
1665
+ await deliver(timer, note, choices, decision.typedSeen, decision.firstHeldAt);
1666
+ return true;
1667
+ });
1499
1668
  }
1500
1669
  const TAIL_AGENTS = 3;
1501
1670
  function stateNowClause(agent) {
@@ -1546,7 +1715,8 @@ function watchedTail(timer) {
1546
1715
  }
1547
1716
  let tail = "";
1548
1717
  try {
1549
- tail = maskChoiceMarker(sanitizeTail(capturePane(agent.tmux_target, tailCaptureLines())));
1718
+ const view = paneClassifierFor(agent.command)?.trailerView(agent.tmux_target) ?? null;
1719
+ tail = maskChoiceMarker(view !== null ? renderTrailerView(view) : sanitizeTail(capturePane(agent.tmux_target, tailCaptureLines())));
1550
1720
  }
1551
1721
  catch {
1552
1722
  }
package/dist/spawn.js CHANGED
@@ -16,6 +16,17 @@ export function withWindowClaim(claim) {
16
16
  }
17
17
  return db.transaction(claim).immediate();
18
18
  }
19
+ export function storedLaunchOptions(value) {
20
+ if (value == null)
21
+ return { args: [], readOnly: false };
22
+ const stored = JSON.parse(value);
23
+ if (Array.isArray(stored))
24
+ return { args: stored, readOnly: false };
25
+ if (stored?.read_only === true && Array.isArray(stored.args)) {
26
+ return { args: stored.args, readOnly: true };
27
+ }
28
+ throw new Error("Invalid stored worker launch options");
29
+ }
19
30
  export function splitTargetWindow(session, projectId, parentActor) {
20
31
  const parent = db
21
32
  .prepare("SELECT tmux_target, tmux_socket FROM agents WHERE actor_id = ? AND status = 'running' ORDER BY id DESC LIMIT 1")
@@ -30,7 +41,7 @@ export function splitTargetWindow(session, projectId, parentActor) {
30
41
  export function buildEnvFlags(env) {
31
42
  return Object.entries(env).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
32
43
  }
33
- function agentIdentityEnv(actorId, name, projectPath) {
44
+ export function agentIdentityEnv(actorId, name, projectPath) {
34
45
  return {
35
46
  HIVE_AGENT_ID: actorId,
36
47
  HIVE_AGENT_NAME: name,
@@ -152,9 +163,11 @@ export function launchAgent(spec) {
152
163
  let info;
153
164
  try {
154
165
  info = db
155
- .prepare("INSERT INTO agents (project_id, name, command, cwd, kind, parent_actor_id, tmux_socket, session_id, codex_home) " +
156
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
157
- .run(spec.projectId, spec.name, typeof spec.commandString === "string" ? spec.commandString : "", spec.cwd, spec.kind, spec.parentActor, socket, spec.sessionId ?? "", spec.codexHome ?? "");
166
+ .prepare("INSERT INTO agents (project_id, name, command, cwd, kind, parent_actor_id, tmux_socket, session_id, codex_home, model, extra_args) " +
167
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
168
+ .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
169
+ ? JSON.stringify({ args: spec.extraArgs ?? [], read_only: true })
170
+ : spec.extraArgs ? JSON.stringify(spec.extraArgs) : null);
158
171
  }
159
172
  catch (e) {
160
173
  throw asNameClash(e, spec.name);
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { noticeFor, releaseNotice } from "./result.js";
2
+ import { noticeFor, releaseNotice, run } from "./result.js";
3
3
  function isBuiltSchema(value) {
4
4
  return typeof value === "object" && value !== null && ("_zod" in value || "_def" in value);
5
5
  }
@@ -51,6 +51,20 @@ function withStructuredContent(name, cb) {
51
51
  };
52
52
  };
53
53
  }
54
+ function withCallGuard(name, cb, guard) {
55
+ return async (...args) => {
56
+ const input = (typeof args[0] === "object" && args[0] !== null ? args[0] : {});
57
+ try {
58
+ guard(name, input);
59
+ }
60
+ catch (e) {
61
+ return run(() => {
62
+ throw e;
63
+ });
64
+ }
65
+ return cb(...args);
66
+ };
67
+ }
54
68
  function withNotice(result) {
55
69
  const notice = noticeFor(result);
56
70
  if (notice && result.structuredContent)
@@ -62,12 +76,15 @@ function withNoticeField(outputSchema) {
62
76
  ? outputSchema
63
77
  : { ...outputSchema, hive_notice: z.string().optional() };
64
78
  }
65
- export function enforceStrictInput(server) {
79
+ export function enforceStrictInput(server, guard) {
66
80
  const register = server.registerTool.bind(server);
67
- server.registerTool = ((name, config, cb) => register(name, {
68
- ...config,
69
- inputSchema: strictSchemaFor(name, config.inputSchema),
70
- outputSchema: withNoticeField(config.outputSchema),
71
- }, (config.outputSchema ? withStructuredContent(name, cb) : cb)));
81
+ server.registerTool = ((name, config, cb) => {
82
+ const guarded = guard ? withCallGuard(name, cb, guard) : cb;
83
+ return register(name, {
84
+ ...config,
85
+ inputSchema: strictSchemaFor(name, config.inputSchema),
86
+ outputSchema: withNoticeField(config.outputSchema),
87
+ }, (config.outputSchema ? withStructuredContent(name, guarded) : guarded));
88
+ });
72
89
  return server;
73
90
  }
package/dist/tmux.js CHANGED
@@ -898,6 +898,52 @@ export function inputBoxState(target) {
898
898
  return null;
899
899
  }
900
900
  }
901
+ const NOISE_ROW = /^[\s\u2500-\u257F\u2580-\u259F\u2800-\u28FF]*$/;
902
+ const FOOTER_KEEP = /esc to interrupt|\d+ (?:shell|background|task|monitor|local agent|team)s?\b|remote dynamic workflow|cloud session|ultraplan needs your input/i;
903
+ const OSC_SEQUENCE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
904
+ const cleanRow = (row) => stripControlBytes(stripSgr(row.replace(OSC_SEQUENCE, ""))).trimEnd();
905
+ function trailerRows(rows, from, to, keep) {
906
+ const out = [];
907
+ for (let i = from; i < to; i++) {
908
+ const row = cleanRow(rows[i]);
909
+ if (NOISE_ROW.test(row))
910
+ continue;
911
+ if (keep !== null && !keep.test(row))
912
+ continue;
913
+ out.push(row.slice(0, TAIL_LINE_CHARS));
914
+ }
915
+ return out;
916
+ }
917
+ export function renderTrailerView(view) {
918
+ const { box } = view;
919
+ const boxLine = box.state === "empty"
920
+ ? "input box: empty"
921
+ : box.state === "ghost"
922
+ ? "input box: empty (a model suggestion is showing; nobody typed it)"
923
+ : box.state === "pending"
924
+ ? `input box: UNSUBMITTED TEXT, not sent: "${box.text}"`
925
+ : "input box: could not be classified";
926
+ return [...view.above.slice(-TAIL_LINES), boxLine, ...view.below].join("\n");
927
+ }
928
+ export function claudeTrailerView(target) {
929
+ try {
930
+ const rows = tmux("capture-pane", "-p", "-e", "-t", target, "-S", `-${tailCaptureLines()}`).split("\n");
931
+ const anchor = findInputBox(rows);
932
+ if (anchor === null)
933
+ return null;
934
+ const box = anchor.prompt === null ? { state: "unknown", text: "" } : classifyInputBox(rows, anchor.prompt, anchor.bottom);
935
+ if (box.state === "unknown")
936
+ return null;
937
+ return {
938
+ above: trailerRows(rows, 0, anchor.top, null),
939
+ box,
940
+ below: trailerRows(rows, anchor.bottom + 1, rows.length, FOOTER_KEEP),
941
+ };
942
+ }
943
+ catch {
944
+ return null;
945
+ }
946
+ }
901
947
  const CHOICE_DIALOG = /Esc to cancel|ctrl\+g to edit in/;
902
948
  const isAwaitingChoiceScreen = (tail, wide) => CHOICE_DIALOG.test(tail) && !inputBoxOnScreen(wide);
903
949
  export const paneHasInputBox = (target) => {
@@ -1056,6 +1102,25 @@ export function codexInputBoxState(target) {
1056
1102
  return null;
1057
1103
  }
1058
1104
  }
1105
+ export function codexTrailerView(target) {
1106
+ try {
1107
+ const rows = tmux("capture-pane", "-p", "-e", "-t", target, "-S", `-${tailCaptureLines()}`).split("\n");
1108
+ const anchor = findCodexPromptBox(rows);
1109
+ if (anchor === null)
1110
+ return null;
1111
+ const box = classifyCodexInputBox(rows, anchor.prompt, anchor.footer);
1112
+ if (box.state === "unknown")
1113
+ return null;
1114
+ return {
1115
+ above: trailerRows(rows, 0, anchor.prompt, null),
1116
+ box,
1117
+ below: trailerRows(rows, anchor.footer, rows.length, FOOTER_KEEP),
1118
+ };
1119
+ }
1120
+ catch {
1121
+ return null;
1122
+ }
1123
+ }
1059
1124
  export const codexPaneHasInputBox = (target) => {
1060
1125
  try {
1061
1126
  return codexInputBoxOnScreen(capturePane(target, tailCaptureLines()));