@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.
- package/README.md +6 -1
- package/claude-plugin/README.md +4 -1
- package/claude-plugin/kickoff.mjs +2 -1
- package/claude-plugin/skills/profile/SKILL.md +78 -0
- package/claude-plugin/skills/profile/references/interview.md +139 -0
- package/claude-plugin/skills/profile/references/recipes/index.md +12 -0
- package/claude-plugin/skills/profile/references/recipes/review-decision.md +41 -0
- package/claude-plugin/skills/profile/references/recipes/session-continuity.md +37 -0
- package/claude-plugin/skills/profile/references/recipes/shared-test-resource.md +45 -0
- package/claude-plugin/skills/profile/references/recipes/verification.md +50 -0
- package/claude-plugin/skills/profile/references/recipes/worker-context-checkpoint.md +51 -0
- package/dist/brief.js +1 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.js +436 -112
- package/dist/codexHome.js +23 -8
- package/dist/config.js +8 -61
- package/dist/context.js +191 -2
- package/dist/db.js +80 -20
- package/dist/firstMessage.js +5 -0
- package/dist/globalConfig.js +247 -0
- package/dist/harnesses.js +14 -4
- package/dist/help.js +29 -11
- package/dist/hook.js +9 -0
- package/dist/hooks.js +2 -1
- package/dist/index.js +11 -5
- package/dist/kickoff.js +42 -29
- package/dist/leadState.js +63 -0
- package/dist/leadWatch.js +73 -0
- package/dist/portfolio.js +210 -0
- package/dist/projectYml.js +242 -178
- package/dist/queenAudit.js +118 -0
- package/dist/queenDashboard.js +573 -0
- package/dist/scheduler.js +190 -20
- package/dist/spawn.js +17 -4
- package/dist/strictInput.js +24 -7
- package/dist/tmux.js +65 -0
- package/dist/tools/agents.js +153 -15
- package/dist/tools/kv.js +24 -0
- package/dist/tools/leases.js +14 -2
- package/dist/tools/meta.js +65 -3
- package/dist/tools/pads.js +42 -0
- package/dist/tools/queenAudit.js +13 -0
- package/dist/tools/todos.js +72 -13
- package/dist/tools/wakes.js +233 -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
|
|
@@ -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
|
-
|
|
1472
|
-
|
|
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
|
-
|
|
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
|
|
1496
|
-
|
|
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
|
-
|
|
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);
|
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) => {
|
|
@@ -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()));
|