@cmgmyr/hive 1.11.1 → 1.11.3
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/claude-plugin/crew/hooks/register.tsx +4 -3
- package/claude-plugin/crew/model.mjs +22 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.js +3 -2
- package/dist/db.js +3 -0
- package/dist/scheduler.js +41 -1
- package/dist/tools/agents.js +17 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { atom, read, update } from 'claude-code'
|
|
2
2
|
import type { EngineInterface, Register } from 'claude-code'
|
|
3
3
|
|
|
4
|
-
import { REFRESH_MS, buildCrewView, failedView,
|
|
4
|
+
import { REFRESH_MS, buildCrewView, failedView, pollCrew } from '../model.mjs'
|
|
5
5
|
import type { CrewView } from '../types'
|
|
6
6
|
|
|
7
7
|
const PANE = 'hive-crew'
|
|
@@ -12,13 +12,14 @@ let polling = false
|
|
|
12
12
|
async function startPolling($: EngineInterface) {
|
|
13
13
|
if (polling) return
|
|
14
14
|
polling = true
|
|
15
|
-
const
|
|
15
|
+
const state = { cwd: await $.session.cwd() }
|
|
16
|
+
const isDir = (dir: string) => $.fs.stat(dir).then(s => s.kind === 'dir', () => false)
|
|
16
17
|
let busy = false
|
|
17
18
|
const refresh = async () => {
|
|
18
19
|
if (busy) return
|
|
19
20
|
busy = true
|
|
20
21
|
try {
|
|
21
|
-
const snapshot = await
|
|
22
|
+
const snapshot = await pollCrew((argv, init) => $.process.run(argv, init), isDir, state, [() => $.session.cwd(), () => $.session.root()])
|
|
22
23
|
await update($, crew, prev => buildCrewView(snapshot, prev, Date.now()))
|
|
23
24
|
} catch (err) {
|
|
24
25
|
await update($, crew, prev => failedView(prev, String(err instanceof Error ? err.message : err)))
|
|
@@ -9,12 +9,33 @@ export function ago(seconds) {
|
|
|
9
9
|
return `${Math.floor(s / 3600)}h${Math.floor((s % 3600) / 60)}m`;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export async function resolveCwd(isDir, candidates) {
|
|
13
|
+
const tried = [];
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
let dir;
|
|
16
|
+
try {
|
|
17
|
+
dir = await candidate();
|
|
18
|
+
} catch {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (typeof dir !== "string" || dir === "" || tried.includes(dir)) continue;
|
|
22
|
+
tried.push(dir);
|
|
23
|
+
if (await isDir(dir)) return dir;
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`hive crew has no directory to run in (tried ${tried.join(", ") || "none"})`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function pollCrew(run, isDir, state, fallbacks) {
|
|
29
|
+
state.cwd = await resolveCwd(isDir, [() => state.cwd, ...fallbacks]);
|
|
30
|
+
return readCrew(run, state.cwd);
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
export async function readCrew(run, cwd) {
|
|
13
34
|
let res;
|
|
14
35
|
try {
|
|
15
36
|
res = await run(["hive", "crew", "--json"], { cwd, timeoutMs: TIMEOUT_MS });
|
|
16
37
|
} catch (err) {
|
|
17
|
-
throw new Error(`hive crew did not answer: ${String(err instanceof Error ? err.message : err).slice(0, 100)}`);
|
|
38
|
+
throw new Error(`hive crew --json in ${cwd} did not answer: ${String(err instanceof Error ? err.message : err).slice(0, 100)}`);
|
|
18
39
|
}
|
|
19
40
|
if (res.exitCode !== 0) {
|
|
20
41
|
const line = String(res.stderr ?? "").split("\n").map((l) => l.trim()).find((l) => l !== "");
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.11.
|
|
1
|
+
{"version":"1.11.3","sha":"959b92c","dirty":false,"build_id":"9ec08cbe1418fce5591b6a2d0373e6bd5173810edd4f572aa06bdd23e49bdd11"}
|
package/dist/cli.js
CHANGED
|
@@ -590,9 +590,10 @@ async function cmdLead(argv) {
|
|
|
590
590
|
try {
|
|
591
591
|
wonRace = db.transaction(() => {
|
|
592
592
|
const updated = db
|
|
593
|
-
.prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ?, command = ?, codex_home =
|
|
593
|
+
.prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ?, command = ?, codex_home = ?, " +
|
|
594
|
+
"pane_started_at = CASE WHEN ? = 1 THEN strftime('%Y-%m-%d %H:%M:%f', 'now') ELSE pane_started_at END " +
|
|
594
595
|
"WHERE id = ? AND tmux_target = ? AND tmux_socket = ? AND pane_pid = ? AND status = 'running'")
|
|
595
|
-
.run(leadPane, tmuxSocketPath(process.env.TMUX, process.env.TMUX_TMPDIR), recordedPanePid, recordedCommand, recordedCodexHome, leadAgentId, casExpected, casSocket, casPid).changes;
|
|
596
|
+
.run(leadPane, tmuxSocketPath(process.env.TMUX, process.env.TMUX_TMPDIR), recordedPanePid, recordedCommand, recordedCodexHome, createdPane ? 1 : 0, leadAgentId, casExpected, casSocket, casPid).changes;
|
|
596
597
|
if (updated === 0)
|
|
597
598
|
return false;
|
|
598
599
|
db.prepare(`UPDATE wakes SET deliver_pane = ?, held_at = NULL, held_reason = NULL
|
package/dist/db.js
CHANGED
|
@@ -573,6 +573,9 @@ CREATE INDEX idx_questions_project_state ON questions(project_id, state, id);
|
|
|
573
573
|
ALTER TABLE queen_audit ADD COLUMN outcome TEXT NOT NULL DEFAULT 'confirmed'
|
|
574
574
|
CHECK (outcome IN ('confirmed', 'refused'));
|
|
575
575
|
ALTER TABLE queen_audit ADD COLUMN problem TEXT NOT NULL DEFAULT '';
|
|
576
|
+
`,
|
|
577
|
+
`
|
|
578
|
+
ALTER TABLE agents ADD COLUMN pane_started_at TEXT;
|
|
576
579
|
`,
|
|
577
580
|
];
|
|
578
581
|
export function storeSchemaAhead(database) {
|
package/dist/scheduler.js
CHANGED
|
@@ -600,6 +600,8 @@ function holdTimer(timer, reason) {
|
|
|
600
600
|
OR (${socketAwaitingWhere()}))`, reason, timer.id, timer.due_at, count, count);
|
|
601
601
|
}
|
|
602
602
|
const HELD_REASON_MODAL_CHOICE = "pane is awaiting a modal choice (folder-trust or /model picker)";
|
|
603
|
+
const LEAD_BOOT_BOX_WAIT_MS = 60_000;
|
|
604
|
+
const HELD_REASON_LEAD_BOOT_INPUT_BOX = "the lead's input box is not on screen yet; holding while its session starts";
|
|
603
605
|
export const HELD_REASON_LEAD_PANE_DEAD = "the lead's pane is not live right now (likely mid-restart); lead-owned wakes are exempt from " +
|
|
604
606
|
"cancellation for this alone, so it is held rather than lost";
|
|
605
607
|
const HELD_REASON_PANE_REISSUED_PREFIX = "the pane id recorded for this wake now belongs to a different pane than the one it was set " +
|
|
@@ -664,6 +666,25 @@ export function isUnsubmittedInputHold(heldReason) {
|
|
|
664
666
|
export const HELD_REASON_CONVERSATION = "a human talked to this lead more recently than the conversation-hold window; holding so a wake " +
|
|
665
667
|
"does not split an in-progress discussion - it delivers once the window passes or the hold's own " +
|
|
666
668
|
"ceiling is reached, whichever comes first";
|
|
669
|
+
function leadBootSessionIsFresh(agentId, panePid) {
|
|
670
|
+
const row = stmt("SELECT pane_started_at FROM agents WHERE id = ? AND pane_pid = ?").get(agentId, panePid);
|
|
671
|
+
if (!row?.pane_started_at)
|
|
672
|
+
return false;
|
|
673
|
+
const timestamp = Date.parse(`${row.pane_started_at.replace(" ", "T")}Z`);
|
|
674
|
+
return Number.isFinite(timestamp) && Date.now() - timestamp < LEAD_BOOT_BOX_WAIT_MS;
|
|
675
|
+
}
|
|
676
|
+
function leadInputBoxObservation(pane, command, cache) {
|
|
677
|
+
const entry = cacheEntry(pane, cache);
|
|
678
|
+
if (entry.snapshot === undefined) {
|
|
679
|
+
entry.snapshot = paneClassifierFor(command)?.inputBoxSnapshot?.(pane) ?? null;
|
|
680
|
+
}
|
|
681
|
+
if (entry.snapshot === null) {
|
|
682
|
+
entry.box = null;
|
|
683
|
+
return { captured: false, box: null };
|
|
684
|
+
}
|
|
685
|
+
entry.box = entry.snapshot.box;
|
|
686
|
+
return { captured: true, box: entry.snapshot.box };
|
|
687
|
+
}
|
|
667
688
|
// A human message keeps a lead-bound wake held for this long after it lands. Measured deferral rates
|
|
668
689
|
// for this and CONVERSATION_HOLD_MAX are in hive-internals/references/worker-state.md.
|
|
669
690
|
const CONVERSATION_HOLD_TTL_SECONDS = 5 * 60;
|
|
@@ -1557,7 +1578,26 @@ function deliverable(timer, snapshot, choices) {
|
|
|
1557
1578
|
noteModalHold(timer, snapshot);
|
|
1558
1579
|
return { ok: false };
|
|
1559
1580
|
}
|
|
1560
|
-
|
|
1581
|
+
const claudeLeadSnapshot = isLeadActorId(timer.deliver_actor) && paneClassifierFor(timer.deliver_command)?.inputBoxSnapshot !== undefined;
|
|
1582
|
+
if (claudeLeadSnapshot) {
|
|
1583
|
+
const bootWindow = row.deliver_row_id !== null && leadBootSessionIsFresh(row.deliver_row_id, row.deliver_pane_pid);
|
|
1584
|
+
const observation = leadInputBoxObservation(timer.deliver_pane, timer.deliver_command, choices);
|
|
1585
|
+
if (!observation.captured) {
|
|
1586
|
+
if (bootWindow)
|
|
1587
|
+
return { ok: false };
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
if (holdsHumanInput(observation.box)) {
|
|
1591
|
+
noteUnsubmittedInputHold(timer, snapshot);
|
|
1592
|
+
return { ok: false };
|
|
1593
|
+
}
|
|
1594
|
+
if (observation.box === null && bootWindow) {
|
|
1595
|
+
holdTimer(timer, HELD_REASON_LEAD_BOOT_INPUT_BOX);
|
|
1596
|
+
return { ok: false };
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
else if (inputBoxHoldsWake(timer.deliver_pane, timer.deliver_command, choices)) {
|
|
1561
1601
|
noteUnsubmittedInputHold(timer, snapshot);
|
|
1562
1602
|
return { ok: false };
|
|
1563
1603
|
}
|
package/dist/tools/agents.js
CHANGED
|
@@ -77,6 +77,23 @@ export function findAgent(projectId, ref) {
|
|
|
77
77
|
const candidates = partial.map((r) => `${r.name} (agent_id ${r.id})`).join(", ");
|
|
78
78
|
throw new Error(`"${ref.name}" matches ${partial.length} running agents: ${candidates}. Use the full name or agent_id.`);
|
|
79
79
|
}
|
|
80
|
+
if (/^\d+$/.test(ref.name)) {
|
|
81
|
+
const id = Number(ref.name);
|
|
82
|
+
if (Number.isSafeInteger(id)) {
|
|
83
|
+
const byId = db
|
|
84
|
+
.prepare("SELECT * FROM agents WHERE project_id = ? AND id = ? AND status = 'running'")
|
|
85
|
+
.get(projectId, id);
|
|
86
|
+
if (byId)
|
|
87
|
+
return byId;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (/^[a-z]+:\d+$/i.test(ref.name)) {
|
|
91
|
+
const byActorId = db
|
|
92
|
+
.prepare("SELECT * FROM agents WHERE project_id = ? AND actor_id = ? AND status = 'running'")
|
|
93
|
+
.get(projectId, ref.name);
|
|
94
|
+
if (byActorId)
|
|
95
|
+
return byActorId;
|
|
96
|
+
}
|
|
80
97
|
throw new Error(`No running agent matching "${ref.name}" in project ${projectId}. Call agent_list.`);
|
|
81
98
|
}
|
|
82
99
|
throw new Error("Pass agent_id or name.");
|
package/package.json
CHANGED