@cmgmyr/hive 1.3.0 → 1.5.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.
- package/README.md +6 -1
- package/claude-plugin/README.md +1 -1
- package/dist/brief.js +1 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.js +419 -56
- package/dist/context.js +193 -2
- package/dist/db.js +61 -18
- package/dist/firstMessage.js +5 -0
- package/dist/harnesses.js +6 -4
- package/dist/help.js +32 -11
- package/dist/hook.js +9 -0
- package/dist/index.js +11 -5
- package/dist/kickoff.js +41 -27
- package/dist/leadState.js +63 -0
- package/dist/leadWatch.js +73 -0
- package/dist/portfolio.js +210 -0
- package/dist/projectRemove.js +68 -0
- package/dist/projectYml.js +19 -0
- package/dist/queenAudit.js +132 -0
- package/dist/queenDashboard.js +573 -0
- package/dist/scheduler.js +179 -15
- package/dist/strictInput.js +24 -7
- package/dist/tmux.js +82 -6
- package/dist/tools/agents.js +4 -0
- package/dist/tools/leases.js +2 -2
- package/dist/tools/meta.js +51 -19
- package/dist/tools/queenAudit.js +13 -0
- package/dist/tools/todos.js +18 -13
- package/dist/tools/wakes.js +197 -65
- package/package.json +4 -4
- package/profiles/orchestration/worker.md +1 -1
- package/profiles/queen/posture.md +31 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -1474,10 +1602,13 @@ async function maybeFireIdle(timer, snapshot, now, choices) {
|
|
|
1474
1602
|
if (snapshot !== null && !timedOut)
|
|
1475
1603
|
noteBlockedWatched(timer, snapshot, choices);
|
|
1476
1604
|
if (timedOut) {
|
|
1477
|
-
|
|
1478
|
-
|
|
1605
|
+
await withPaneClaim(timer, async () => {
|
|
1606
|
+
const decision = deliverable(timer, snapshot, choices);
|
|
1607
|
+
if (!decision.ok || !claimOneShot(timer))
|
|
1608
|
+
return false;
|
|
1479
1609
|
await deliver(timer, STANDING_EXPIRED_NOTE, choices, decision.typedSeen, decision.firstHeldAt);
|
|
1480
|
-
|
|
1610
|
+
return true;
|
|
1611
|
+
});
|
|
1481
1612
|
}
|
|
1482
1613
|
return;
|
|
1483
1614
|
}
|
|
@@ -1497,12 +1628,44 @@ async function maybeFireIdle(timer, snapshot, now, choices) {
|
|
|
1497
1628
|
noteBlockedWatched(timer, snapshot, choices);
|
|
1498
1629
|
}
|
|
1499
1630
|
if (ready) {
|
|
1500
|
-
|
|
1501
|
-
|
|
1631
|
+
await withPaneClaim(timer, async () => {
|
|
1632
|
+
const decision = deliverable(timer, snapshot, choices);
|
|
1633
|
+
if (!decision.ok || !claimOneShot(timer))
|
|
1634
|
+
return false;
|
|
1502
1635
|
await deliver(timer, timedOut && !idleMet ? "max wait reached" : "", choices, decision.typedSeen, decision.firstHeldAt);
|
|
1503
|
-
|
|
1636
|
+
return true;
|
|
1637
|
+
});
|
|
1504
1638
|
}
|
|
1505
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 () => {
|
|
1659
|
+
const decision = deliverable(timer, snapshot, choices);
|
|
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);
|
|
1664
|
+
}
|
|
1665
|
+
await deliver(timer, note, choices, decision.typedSeen, decision.firstHeldAt);
|
|
1666
|
+
return true;
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1506
1669
|
const TAIL_AGENTS = 3;
|
|
1507
1670
|
function stateNowClause(agent) {
|
|
1508
1671
|
if (!reportsAgentStateLog(agent))
|
|
@@ -1552,7 +1715,8 @@ function watchedTail(timer) {
|
|
|
1552
1715
|
}
|
|
1553
1716
|
let tail = "";
|
|
1554
1717
|
try {
|
|
1555
|
-
|
|
1718
|
+
const view = paneClassifierFor(agent.command)?.trailerView(agent.tmux_target) ?? null;
|
|
1719
|
+
tail = maskChoiceMarker(view !== null ? renderTrailerView(view) : sanitizeTail(capturePane(agent.tmux_target, tailCaptureLines())));
|
|
1556
1720
|
}
|
|
1557
1721
|
catch {
|
|
1558
1722
|
}
|
package/dist/strictInput.js
CHANGED
|
@@ -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) =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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) => {
|
|
@@ -972,7 +1018,8 @@ export function paneChoiceCheck(target) {
|
|
|
972
1018
|
// codex has no box border to anchor on, so the box's bottom edge used to be inferred from the literal
|
|
973
1019
|
// text of its footer status line - but that text is not stable (todo 524: two different compositions
|
|
974
1020
|
// seen on one codex version, one with no shared substring at all). The anchor below reads structure
|
|
975
|
-
// instead of content: the pane's own last non-blank row IS the bottom edge, whatever it says
|
|
1021
|
+
// instead of content: the pane's own last non-blank row IS the bottom edge, whatever it says, except that a
|
|
1022
|
+
// "? for shortcuts" row or a right-aligned footer row pulls the edge up to the status row above it.
|
|
976
1023
|
// codex's own choice-menu shape (see hive-internals), not claude's CHOICE_DIALOG wording. Declared
|
|
977
1024
|
// above findCodexPromptBox because the box search itself now needs it to reject a choice row rather
|
|
978
1025
|
// than mistake it for a live prompt.
|
|
@@ -991,6 +1038,7 @@ function codexPromptContentStart(promptRow) {
|
|
|
991
1038
|
i += 1;
|
|
992
1039
|
return i;
|
|
993
1040
|
}
|
|
1041
|
+
const CODEX_SHORTCUTS_ROW = /^\?\s+for shortcuts/;
|
|
994
1042
|
function findCodexPromptBox(rows) {
|
|
995
1043
|
const text = rows.map((row) => stripControlBytes(stripSgr(row)).trim());
|
|
996
1044
|
let footer = text.length - 1;
|
|
@@ -998,6 +1046,17 @@ function findCodexPromptBox(rows) {
|
|
|
998
1046
|
footer -= 1;
|
|
999
1047
|
if (footer < 0)
|
|
1000
1048
|
return null;
|
|
1049
|
+
// The row directly above "? for shortcuts" starts one footer block if it has codex's " · " status separator.
|
|
1050
|
+
if (CODEX_SHORTCUTS_ROW.test(text[footer]) && footer > 0 && text[footer - 1].includes(" · "))
|
|
1051
|
+
footer -= 1;
|
|
1052
|
+
else if (footer > 0 && text[footer - 1].includes(" · ")) {
|
|
1053
|
+
const footerRow = stripControlBytes(stripSgr(rows[footer]));
|
|
1054
|
+
const footerContent = footerRow.trim();
|
|
1055
|
+
const leadingWhitespace = footerRow.length - footerRow.trimStart().length;
|
|
1056
|
+
// Use the row's geometry so this remains independent of whatever Codex writes in its footer.
|
|
1057
|
+
if (leadingWhitespace > footerContent.length)
|
|
1058
|
+
footer -= 1;
|
|
1059
|
+
}
|
|
1001
1060
|
let prompt = -1;
|
|
1002
1061
|
for (let i = footer - 1; i >= 0 && footer - i <= BOX_MAX_ROWS; i--) {
|
|
1003
1062
|
// The arrow ALONE, against the trimmed row: capture-pane strips a trailing space, and a box with
|
|
@@ -1016,12 +1075,10 @@ function findCodexPromptBox(rows) {
|
|
|
1016
1075
|
// stale submitted prompt that also starts with "›").
|
|
1017
1076
|
if (CODEX_CHOICE_LINE.test(text[prompt]))
|
|
1018
1077
|
return null;
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
// box - a busy/mid-turn screen can have an old prompt sitting within BOX_MAX_ROWS of whatever text
|
|
1022
|
-
// is currently at the bottom, and only this check tells the two apart (see hive-internals).
|
|
1078
|
+
// Only indented continuation rows, then one blank gap, may sit between the prompt and footer.
|
|
1079
|
+
// Left-aligned output in that gap means the prompt is stale, not a live box (see hive-internals).
|
|
1023
1080
|
let i = prompt + 1;
|
|
1024
|
-
while (i <= footer && text[i] !== "")
|
|
1081
|
+
while (i <= footer && text[i] !== "" && /^\s/.test(stripControlBytes(stripSgr(rows[i]))))
|
|
1025
1082
|
i += 1;
|
|
1026
1083
|
while (i <= footer && text[i] === "")
|
|
1027
1084
|
i += 1;
|
|
@@ -1056,6 +1113,25 @@ export function codexInputBoxState(target) {
|
|
|
1056
1113
|
return null;
|
|
1057
1114
|
}
|
|
1058
1115
|
}
|
|
1116
|
+
export function codexTrailerView(target) {
|
|
1117
|
+
try {
|
|
1118
|
+
const rows = tmux("capture-pane", "-p", "-e", "-t", target, "-S", `-${tailCaptureLines()}`).split("\n");
|
|
1119
|
+
const anchor = findCodexPromptBox(rows);
|
|
1120
|
+
if (anchor === null)
|
|
1121
|
+
return null;
|
|
1122
|
+
const box = classifyCodexInputBox(rows, anchor.prompt, anchor.footer);
|
|
1123
|
+
if (box.state === "unknown")
|
|
1124
|
+
return null;
|
|
1125
|
+
return {
|
|
1126
|
+
above: trailerRows(rows, 0, anchor.prompt, null),
|
|
1127
|
+
box,
|
|
1128
|
+
below: trailerRows(rows, anchor.footer, rows.length, FOOTER_KEEP),
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
catch {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1059
1135
|
export const codexPaneHasInputBox = (target) => {
|
|
1060
1136
|
try {
|
|
1061
1137
|
return codexInputBoxOnScreen(capturePane(target, tailCaptureLines()));
|
package/dist/tools/agents.js
CHANGED
|
@@ -2,6 +2,8 @@ import { existsSync, rmSync, statSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { db } from "../db.js";
|
|
5
|
+
import { confirmQueenWrite } from "../queenAudit.js";
|
|
6
|
+
import { captureQueenWriteIdentity } from "../context.js";
|
|
5
7
|
import { agentBriefPath, mergedBriefVars, readAgentBrief, workerBrief, workerCommandString, writeAgentBrief, } from "../brief.js";
|
|
6
8
|
import { codexHomeDir, codexInstructionsPhrase, codexLaunchArgs, ensureCodexHome, ensureCodexHooksFile, refreshCodexMcpEnv } from "../codexHome.js";
|
|
7
9
|
import { currentActor, findProjectForDir, getProject, linkedWorktreePrimaryRoot, resolveProject } from "../context.js";
|
|
@@ -1067,6 +1069,7 @@ export function registerAgents(server) {
|
|
|
1067
1069
|
const agent = findAgent(project.id, args);
|
|
1068
1070
|
requireLive(agent);
|
|
1069
1071
|
const target = agent.tmux_target;
|
|
1072
|
+
const auditIdentity = captureQueenWriteIdentity(project.id);
|
|
1070
1073
|
let outgoing = args.text ?? "";
|
|
1071
1074
|
const tag = senderTag(project.id, currentActor());
|
|
1072
1075
|
let shortened = null;
|
|
@@ -1210,6 +1213,7 @@ export function registerAgents(server) {
|
|
|
1210
1213
|
: `That target is a LEAD session, so agent_send's keys path is refused against it from a non-lead caller: you cannot finish this yourself. A human at that terminal, or another lead, has to press Enter there. Leave it and try again later.${shortened === null ? "" : " Pressing Enter there is the INTENDED finish, not a stray paste: that stranded line is hive's own pointer, and submitting it is exactly what this send was supposed to do."}`) +
|
|
1211
1214
|
shortenedClause, { cause: err });
|
|
1212
1215
|
}
|
|
1216
|
+
confirmQueenWrite("agent_send", project.id, args, { sent: true, agent_id: agent.id }, auditIdentity);
|
|
1213
1217
|
}
|
|
1214
1218
|
else {
|
|
1215
1219
|
throw new Error("Pass text or keys.");
|
package/dist/tools/leases.js
CHANGED
|
@@ -18,7 +18,7 @@ export function extendOwnedLease(projectId, key, actor, ttlSeconds) {
|
|
|
18
18
|
}
|
|
19
19
|
export function registerLeases(server) {
|
|
20
20
|
server.registerTool("lease_acquire", {
|
|
21
|
-
description: "Try to take a named lease on a shared
|
|
21
|
+
description: "Try to take a named lease on state another session could also change, such as a shared checkout, a dev database or a port (non-blocking). Re-taking your own lease extends it. Leases expire on their own.",
|
|
22
22
|
annotations: {
|
|
23
23
|
readOnlyHint: false,
|
|
24
24
|
destructiveHint: false,
|
|
@@ -26,7 +26,7 @@ export function registerLeases(server) {
|
|
|
26
26
|
openWorldHint: false,
|
|
27
27
|
},
|
|
28
28
|
inputSchema: {
|
|
29
|
-
key: z.string().describe('Stable and specific, like "
|
|
29
|
+
key: z.string().describe('Stable and specific, like "db:dev".'),
|
|
30
30
|
ttl_seconds: z.number().int().positive(),
|
|
31
31
|
project_id: projectIdParam,
|
|
32
32
|
},
|
package/dist/tools/meta.js
CHANGED
|
@@ -1,33 +1,29 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { db } from "../db.js";
|
|
3
|
-
import { addProject, currentActor, listProjects, resolveProject, selectProjectById, TOUCH_INTERVAL_MS, trySelectedProject, } from "../context.js";
|
|
3
|
+
import { addProject, currentActor, listProjects, findProjectForCwd, resolveProject, selectProjectById, TOUCH_INTERVAL_MS, trySelectedProject, } from "../context.js";
|
|
4
|
+
import { confirmQueenWrite } from "../queenAudit.js";
|
|
4
5
|
import { errorMessage, run } from "../result.js";
|
|
5
6
|
import { HELP_TOPICS, helpOverview } from "../help.js";
|
|
6
7
|
import { idParam } from "./params.js";
|
|
8
|
+
import { formatRowCounts, PROJECT_OWNER_TABLES, projectRowCounts, removeProject } from "../projectRemove.js";
|
|
7
9
|
function refuseIfLocked(tool) {
|
|
8
10
|
if (process.env.HIVE_PROJECT_LOCK === "1") {
|
|
9
11
|
throw new Error(`${tool} sweeps the whole store, across every project and every actor. This session is locked to one project (HIVE_PROJECT_LOCK=1) and cannot run it. Run it from an unlocked session (a lead).`);
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
|
-
export const PROJECT_OWNER_TABLES = [
|
|
13
|
-
"pads",
|
|
14
|
-
"todos",
|
|
15
|
-
"kv",
|
|
16
|
-
"leases",
|
|
17
|
-
"agents",
|
|
18
|
-
"wakes",
|
|
19
|
-
"command_trust",
|
|
20
|
-
];
|
|
21
14
|
function existsWhere(table, column, value) {
|
|
22
15
|
return db.prepare(`SELECT 1 FROM ${table} WHERE ${column} = ? LIMIT 1`).get(value) !== undefined;
|
|
23
16
|
}
|
|
24
17
|
function projectOwnsRows(projectId) {
|
|
25
18
|
return PROJECT_OWNER_TABLES.some((table) => existsWhere(table, "project_id", projectId));
|
|
26
19
|
}
|
|
27
|
-
const pruneProjectIfEmpty = db.transaction((
|
|
28
|
-
if (projectOwnsRows(
|
|
20
|
+
const pruneProjectIfEmpty = db.transaction((project) => {
|
|
21
|
+
if (projectOwnsRows(project.id))
|
|
22
|
+
return false;
|
|
23
|
+
if (db.prepare("DELETE FROM projects WHERE id = ?").run(project.id).changes === 0)
|
|
29
24
|
return false;
|
|
30
|
-
|
|
25
|
+
confirmQueenWrite("project_prune", project.id, {}, { project_id: project.id, name: project.name });
|
|
26
|
+
return true;
|
|
31
27
|
});
|
|
32
28
|
export const ACTOR_OWNER_COLUMNS = [
|
|
33
29
|
["agents", "actor_id"],
|
|
@@ -143,29 +139,65 @@ export function registerMeta(server) {
|
|
|
143
139
|
inputSchema: { project_id: idParam },
|
|
144
140
|
}, ({ project_id }) => run(() => selectProjectById(project_id)));
|
|
145
141
|
server.registerTool("project_prune", {
|
|
146
|
-
description: "Delete every registered project that owns no rows anywhere in the store (pads, todos, kv, leases, agents, wakes, command_trust), verified individually before each delete. Never prunes the caller's own project. Refuses under HIVE_PROJECT_LOCK=1: this is a whole-store sweep, and a project-locked session may only touch its own project. Immediate, permanent: no dry-run mode.",
|
|
142
|
+
description: "Delete every registered project that owns no rows anywhere in the store (pads, todos, kv, leases, agents, wakes, command_trust), verified individually before each delete. Never prunes the caller's own project. With project_id, removes exactly that one project instead of sweeping, and only if it owns no rows; otherwise refuses and names what it owns. With project_id AND confirm_name (the project's exact name), removes that one project even when it owns rows: it takes a snapshot first, refuses while the project has a running agent, then deletes the project with everything it owns (todos, comments, pads, kv, leases, agents, wakes, trust, agent messages) in one transaction and reports per-table counts and the snapshot path. queen_audit rows naming it are kept, and so are other projects' lead-idle subscriptions that target it, so a watch on its lead ends with a named reason. confirm_name without project_id is refused, and there is no forced sweep. Refuses under HIVE_PROJECT_LOCK=1: this is a whole-store sweep, and a project-locked session may only touch its own project. Immediate, permanent: no dry-run mode.",
|
|
147
143
|
annotations: {
|
|
148
144
|
readOnlyHint: false,
|
|
149
145
|
destructiveHint: true,
|
|
150
146
|
idempotentHint: true,
|
|
151
147
|
openWorldHint: false,
|
|
152
148
|
},
|
|
153
|
-
inputSchema: {},
|
|
149
|
+
inputSchema: { project_id: idParam.optional(), confirm_name: z.string().optional() },
|
|
154
150
|
outputSchema: {
|
|
155
151
|
deleted: z.array(z.object({ id: idParam, name: z.string() })),
|
|
156
152
|
held_back: z.object({ project_id: idParam, reason: z.string() }),
|
|
157
153
|
errors: z.array(z.object({ id: idParam, name: z.string(), error: z.string() })).optional(),
|
|
154
|
+
counts: z.record(z.string(), z.number()).optional(),
|
|
155
|
+
snapshot: z.string().nullable().optional(),
|
|
158
156
|
},
|
|
159
|
-
}, () => run(() => {
|
|
157
|
+
}, ({ project_id, confirm_name }) => run(() => {
|
|
160
158
|
refuseIfLocked("project_prune");
|
|
161
159
|
const homeId = resolveProject().id;
|
|
160
|
+
const cwdId = findProjectForCwd()?.id;
|
|
161
|
+
const isOwn = (id) => id === homeId || id === cwdId;
|
|
162
|
+
if (confirm_name !== undefined && project_id === undefined) {
|
|
163
|
+
throw new Error("project_prune: confirm_name needs project_id; there is no forced sweep.");
|
|
164
|
+
}
|
|
165
|
+
if (project_id !== undefined) {
|
|
166
|
+
if (isOwn(project_id))
|
|
167
|
+
throw new Error(`project_prune: project ${project_id} is the caller's own project and is never pruned.`);
|
|
168
|
+
const target = listProjects().find((p) => p.id === project_id);
|
|
169
|
+
if (!target)
|
|
170
|
+
throw new Error(`project_prune: no project ${project_id}. List them with project_list.`);
|
|
171
|
+
if (confirm_name !== undefined) {
|
|
172
|
+
if (confirm_name !== target.name) {
|
|
173
|
+
throw new Error(`project_prune: confirm_name does not match project ${project_id}, whose name is "${target.name}". Nothing deleted.`);
|
|
174
|
+
}
|
|
175
|
+
const removed = removeProject(project_id, {
|
|
176
|
+
snapshot: true,
|
|
177
|
+
onRemoved: (r) => confirmQueenWrite("project_prune", project_id, {}, { project_id, name: r.deleted.name, counts: r.counts }),
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
deleted: [{ id: removed.deleted.id, name: removed.deleted.name }],
|
|
181
|
+
held_back: { project_id: homeId, reason: "caller's own project" },
|
|
182
|
+
counts: removed.counts,
|
|
183
|
+
snapshot: removed.snapshot,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (!pruneProjectIfEmpty.immediate(target)) {
|
|
187
|
+
throw new Error(`project_prune: project ${project_id} ("${target.name}") is not empty, nothing deleted. It owns ${formatRowCounts(projectRowCounts(project_id))}. To remove it with everything it owns, pass confirm_name with its exact name.`);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
deleted: [{ id: target.id, name: target.name }],
|
|
191
|
+
held_back: { project_id: homeId, reason: "caller's own project" },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
162
194
|
const deleted = [];
|
|
163
195
|
const errors = [];
|
|
164
196
|
for (const project of listProjects()) {
|
|
165
|
-
if (project.id
|
|
197
|
+
if (isOwn(project.id))
|
|
166
198
|
continue;
|
|
167
199
|
try {
|
|
168
|
-
if (pruneProjectIfEmpty.immediate(project
|
|
200
|
+
if (pruneProjectIfEmpty.immediate(project)) {
|
|
169
201
|
deleted.push({ id: project.id, name: project.name });
|
|
170
202
|
}
|
|
171
203
|
}
|
|
@@ -180,7 +212,7 @@ export function registerMeta(server) {
|
|
|
180
212
|
};
|
|
181
213
|
}));
|
|
182
214
|
server.registerTool("actor_prune", {
|
|
183
|
-
description: "Delete every actor that owns no rows anywhere in the store and has not been active in the last minute: agents.actor_id, agents.parent_actor_id, todos.locked_by, todo_comments.author, kv.updated_by, leases.owner, pads.updated_by, wakes.owner, wakes.deliver_actor, agent_state_log.actor_id. The scan is global across every project, never scoped to the caller's: actors carry no project_id, so an actor can own rows in a project the caller cannot see, and a project-scoped scan would misread that actor as inert and delete it. Never prunes the caller's own actor. Refuses under HIVE_PROJECT_LOCK=1: this is a whole-store sweep. Run this after project_prune when sweeping the store:
|
|
215
|
+
description: "Delete every actor that owns no rows anywhere in the store and has not been active in the last minute: agents.actor_id, agents.parent_actor_id, todos.locked_by, todo_comments.author, kv.updated_by, leases.owner, pads.updated_by, wakes.owner, wakes.deliver_actor, agent_state_log.actor_id. The scan is global across every project, never scoped to the caller's: actors carry no project_id, so an actor can own rows in a project the caller cannot see, and a project-scoped scan would misread that actor as inert and delete it. Never prunes the caller's own actor. Refuses under HIVE_PROJECT_LOCK=1: this is a whole-store sweep. Run this after project_prune when sweeping the store: project_prune with confirm_name removes a non-empty project together with its agents rows, so run this afterwards to clear that project's actors that own nothing else. Actors with agent_state_log rows stay, because that log is append-only and has no project, which means every actor that ever ran a lead or worker stays. Immediate, permanent: no dry-run mode.",
|
|
184
216
|
annotations: {
|
|
185
217
|
readOnlyHint: false,
|
|
186
218
|
destructiveHint: true,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { listQueenAudit } from "../queenAudit.js";
|
|
2
|
+
import { run } from "../result.js";
|
|
3
|
+
import { idParam, limitParam } from "./params.js";
|
|
4
|
+
export function registerQueenAudit(server) {
|
|
5
|
+
server.registerTool("queen_audit_list", {
|
|
6
|
+
description: "List confirmed queen writes into other projects, newest first. The queen may read all targets; other callers see only their own project. Default 20, maximum 100 entries. History is retained for 30 days with a 20,000 id-range backstop. A crash between a completed terminal send and its audit insert can leave that send unrecorded.",
|
|
7
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
8
|
+
inputSchema: {
|
|
9
|
+
project_id: idParam.optional().describe("Filter by the target project id. Only the queen may name another project."),
|
|
10
|
+
limit: limitParam,
|
|
11
|
+
},
|
|
12
|
+
}, (args) => run(() => listQueenAudit(args)));
|
|
13
|
+
}
|