@cmgmyr/hive 1.3.0 → 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 +1 -1
- package/dist/brief.js +1 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.js +360 -55
- package/dist/context.js +191 -2
- package/dist/db.js +61 -18
- package/dist/firstMessage.js +5 -0
- package/dist/harnesses.js +6 -4
- package/dist/help.js +28 -10
- 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/projectYml.js +19 -0
- package/dist/queenAudit.js +118 -0
- package/dist/queenDashboard.js +573 -0
- package/dist/scheduler.js +179 -15
- package/dist/strictInput.js +24 -7
- package/dist/tmux.js +65 -0
- package/dist/tools/agents.js +4 -0
- package/dist/tools/leases.js +2 -2
- package/dist/tools/meta.js +23 -3
- 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/tools/wakes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { db } from "../db.js";
|
|
3
|
-
import {
|
|
3
|
+
import { commitQueenStateWrite } from "../queenAudit.js";
|
|
4
|
+
import { currentActor, effectiveProjectId, getProject, isQueenLead } from "../context.js";
|
|
4
5
|
import { run } from "../result.js";
|
|
5
6
|
import { findAgent, isLive, probeFailed, summaryLiveness } from "./agents.js";
|
|
6
7
|
import { ACTIVE_TIMER_WHERE, LOG_RETENTION, OWNED_BY_WATCH, WATCH_SCOPE_PROJECT, seedGoneCursor, } from "../scheduler.js";
|
|
@@ -8,7 +9,9 @@ import { idParam, projectIdParam } from "./params.js";
|
|
|
8
9
|
import { cutToUnitBudget } from "../slug.js";
|
|
9
10
|
import { awaitingFirstPrompt } from "../firstPrompt.js";
|
|
10
11
|
import { deriveProvenance } from "../stateProvenance.js";
|
|
11
|
-
import { findUnsafeControlChar, liveTargets, TEXT_ALLOWED_CONTROL_CHARS } from "../tmux.js";
|
|
12
|
+
import { findUnsafeControlChar, liveTargets, paneReissued, rowAliveProbe, TEXT_ALLOWED_CONTROL_CHARS, } from "../tmux.js";
|
|
13
|
+
import { readLeadTurnState } from "../leadState.js";
|
|
14
|
+
import { LEAD_TARGET_GONE, LEAD_TARGET_REISSUED, leadSubscription, paneVeto, runningLeadOf } from "../leadWatch.js";
|
|
12
15
|
import { isRunningLeadActor, LEAD_KIND } from "../spawn.js";
|
|
13
16
|
import { commandHead, screenClassifiable } from "../harnesses.js";
|
|
14
17
|
const agentRefParam = z
|
|
@@ -125,6 +128,7 @@ const baseWakeFields = (t, { truncate = true } = {}) => ({
|
|
|
125
128
|
deliver_to: t.deliver_actor,
|
|
126
129
|
...(t.watch_scope ? { scope: t.watch_scope, standing: true } : {}),
|
|
127
130
|
...(t.parent_wake_id != null ? { parent_wake_id: t.parent_wake_id } : {}),
|
|
131
|
+
...leadWatchFields(t),
|
|
128
132
|
});
|
|
129
133
|
const STANDING_WATCH_LIFETIME_SECONDS = 4 * 60 * 60;
|
|
130
134
|
const openStandingWatch = db.transaction((projectId, owner, body, deliverActor, deliverPane, maxWait) => {
|
|
@@ -172,6 +176,112 @@ function createStandingWatch(projectId, args) {
|
|
|
172
176
|
"finished before the watch was set is exactly the finish a one-shot loses. Go quiet.",
|
|
173
177
|
};
|
|
174
178
|
}
|
|
179
|
+
export const LEAD_WATCH_QUEEN_ONLY = "LEAD_WATCH_QUEEN_ONLY";
|
|
180
|
+
const openLeadWatch = db.transaction((homeProjectId, body, mode, delivery, maxWait, target) => {
|
|
181
|
+
const state = readLeadTurnState(target.agentId);
|
|
182
|
+
const sameLaunch = state !== null && state.pane_pid === target.panePid;
|
|
183
|
+
const sessionId = sameLaunch && state.state !== "unknown" ? state.session_id : "";
|
|
184
|
+
const baseline = state?.idle_seq ?? 0;
|
|
185
|
+
const row = db
|
|
186
|
+
.prepare(`INSERT INTO wakes (project_id, owner, body, kind, watch, deliver_actor, deliver_pane, max_wait_at)
|
|
187
|
+
VALUES (?, ?, ?, ?, '[]', ?, ?, datetime('now', printf('+%d seconds', ?)))
|
|
188
|
+
RETURNING id`)
|
|
189
|
+
.get(homeProjectId, currentActor(), body, `idle_${mode}`, delivery.actor, delivery.pane, maxWait);
|
|
190
|
+
db.prepare(`INSERT INTO lead_idle_subscriptions (wake_id, target_project_id, agent_id, pane_pid, session_id, baseline_idle_seq)
|
|
191
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(row.id, target.projectId, target.agentId, target.panePid, sessionId, baseline);
|
|
192
|
+
return { id: row.id, session_id: sessionId, baseline_idle_seq: baseline };
|
|
193
|
+
});
|
|
194
|
+
function createLeadWatch(leadProjectId, args) {
|
|
195
|
+
if (!isQueenLead()) {
|
|
196
|
+
throw new Error(`${LEAD_WATCH_QUEEN_ONLY}: lead_project_id is the queen's alone. Only the running queen lead, in its own ` +
|
|
197
|
+
"project and without HIVE_PROJECT_LOCK, may wait on another project's lead; a project's own lead and " +
|
|
198
|
+
"every worker are refused. A worker is watched with agents=[...] or scope=\"project\" instead.");
|
|
199
|
+
}
|
|
200
|
+
if (args.project_id != null || args.deliver_to != null) {
|
|
201
|
+
throw new Error("lead_project_id refuses project_id and deliver_to: the wake is always stored in the queen's own project " +
|
|
202
|
+
"and delivered to the queen's own pane, so neither has anything to choose. Name the watched project " +
|
|
203
|
+
"with lead_project_id alone.");
|
|
204
|
+
}
|
|
205
|
+
const home = effectiveProjectId();
|
|
206
|
+
if (leadProjectId === home) {
|
|
207
|
+
throw new Error("lead_project_id names the queen's own project, and the queen cannot wait on itself.");
|
|
208
|
+
}
|
|
209
|
+
const project = getProject(leadProjectId);
|
|
210
|
+
if (!project)
|
|
211
|
+
throw new Error(`Unknown project_id ${leadProjectId}. Call project_list to see options.`);
|
|
212
|
+
const lead = runningLeadOf(leadProjectId);
|
|
213
|
+
if (!lead)
|
|
214
|
+
throw new Error(`${LEAD_TARGET_GONE}: project ${leadProjectId} ("${project.name}") has no running lead to watch.`);
|
|
215
|
+
if (!screenClassifiable(lead.command)) {
|
|
216
|
+
throw new Error(`Project ${leadProjectId}'s lead runs ${JSON.stringify(commandHead(lead.command))}, whose screen hive cannot ` +
|
|
217
|
+
"classify, so a turn ending while a human is typing to it could not be told apart from one nobody is at.");
|
|
218
|
+
}
|
|
219
|
+
if (lead.pane_pid === "") {
|
|
220
|
+
throw new Error(`${LEAD_TARGET_REISSUED}: project ${leadProjectId}'s lead has no recorded pane pid, so a reused pane id ` +
|
|
221
|
+
"could not be told from this lead. Restart it with hive lead to record one.");
|
|
222
|
+
}
|
|
223
|
+
const snapshot = liveTargets();
|
|
224
|
+
if (snapshot === null)
|
|
225
|
+
throw new Error("tmux did not answer, so the watched lead's pane cannot be verified. Try again.");
|
|
226
|
+
const probe = rowAliveProbe(lead.tmux_socket, lead.tmux_target, snapshot);
|
|
227
|
+
if (probe.live === null) {
|
|
228
|
+
throw new Error(`Project ${leadProjectId}'s lead lives on a tmux socket this process cannot probe.`);
|
|
229
|
+
}
|
|
230
|
+
if (!probe.live)
|
|
231
|
+
throw new Error(`${LEAD_TARGET_GONE}: project ${leadProjectId}'s lead pane is gone.`);
|
|
232
|
+
if (paneReissued(lead.pane_pid, probe)) {
|
|
233
|
+
throw new Error(`${LEAD_TARGET_REISSUED}: project ${leadProjectId}'s lead pane id now belongs to another process.`);
|
|
234
|
+
}
|
|
235
|
+
const mode = args.mode ?? "any";
|
|
236
|
+
const state = readLeadTurnState(lead.id);
|
|
237
|
+
const idleNow = state !== null && state.pane_pid === lead.pane_pid && state.state === "idle";
|
|
238
|
+
if (mode === "all" && idleNow && paneVeto(lead) === null) {
|
|
239
|
+
return {
|
|
240
|
+
status: "already_satisfied",
|
|
241
|
+
note: `Project ${leadProjectId}'s lead has already ended its turn; nothing was scheduled. Read it now.`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const delivery = resolveDelivery(home);
|
|
245
|
+
const maxWait = args.max_wait_seconds ?? 900;
|
|
246
|
+
const row = openLeadWatch.immediate(home, args.body, mode, delivery, maxWait, {
|
|
247
|
+
projectId: leadProjectId,
|
|
248
|
+
agentId: lead.id,
|
|
249
|
+
panePid: lead.pane_pid,
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
wake_id: row.id,
|
|
253
|
+
mode,
|
|
254
|
+
lead_watch: {
|
|
255
|
+
project_id: leadProjectId,
|
|
256
|
+
agent_id: lead.id,
|
|
257
|
+
name: lead.name,
|
|
258
|
+
state: state !== null && state.pane_pid === lead.pane_pid ? state.state : "unknown",
|
|
259
|
+
baseline_idle_seq: row.baseline_idle_seq,
|
|
260
|
+
},
|
|
261
|
+
max_wait_seconds: maxWait,
|
|
262
|
+
deliver_to: delivery.actor,
|
|
263
|
+
note: (mode === "any"
|
|
264
|
+
? "Fires on the lead's NEXT turn ending; a turn that already ended does not count. "
|
|
265
|
+
: "Fires once the lead's turn has ended. ") +
|
|
266
|
+
"A turn ending is not the lead finishing its work: read it before acting. A dead, restarted or reissued " +
|
|
267
|
+
"lead pane ends the watch with that reason instead.",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function leadWatchFields(t) {
|
|
271
|
+
const sub = leadSubscription(t.id);
|
|
272
|
+
if (sub === null)
|
|
273
|
+
return {};
|
|
274
|
+
return {
|
|
275
|
+
lead_watch: {
|
|
276
|
+
project_id: sub.target_project_id,
|
|
277
|
+
agent_id: sub.agent_id,
|
|
278
|
+
pane_pid: sub.pane_pid,
|
|
279
|
+
session_id: sub.session_id,
|
|
280
|
+
baseline_idle_seq: sub.baseline_idle_seq,
|
|
281
|
+
...(sub.terminal_reason !== null ? { terminal_reason: sub.terminal_reason } : {}),
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
175
285
|
export function registerWakes(server) {
|
|
176
286
|
server.registerTool("wake_set", {
|
|
177
287
|
description: "Schedule a wake-up: after delay_seconds the body is typed into the target session's terminal " +
|
|
@@ -202,20 +312,22 @@ export function registerWakes(server) {
|
|
|
202
312
|
rejectUnsafeBody(args.body);
|
|
203
313
|
const projectId = effectiveProjectId(args.project_id);
|
|
204
314
|
const delivery = resolveDelivery(projectId, args.deliver_to);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
315
|
+
return commitQueenStateWrite("wake_set", projectId, args, () => {
|
|
316
|
+
const row = db
|
|
317
|
+
.prepare(`INSERT INTO wakes (project_id, owner, body, kind, deliver_actor, deliver_pane, due_at, repeat_every_ms)
|
|
318
|
+
VALUES (?, ?, ?, 'delay', ?, ?, datetime('now', printf('+%d seconds', ?)), ?)
|
|
319
|
+
RETURNING id, due_at`)
|
|
320
|
+
.get(projectId, currentActor(), args.body, delivery.actor, delivery.pane, args.delay_seconds, args.repeat_every_seconds != null ? args.repeat_every_seconds * 1000 : null);
|
|
321
|
+
return {
|
|
322
|
+
wake_id: row.id,
|
|
323
|
+
due_at: row.due_at,
|
|
324
|
+
deliver_to: delivery.actor,
|
|
325
|
+
repeating: args.repeat_every_seconds != null,
|
|
326
|
+
};
|
|
327
|
+
});
|
|
216
328
|
}));
|
|
217
329
|
server.registerTool("wake_when_idle", {
|
|
218
|
-
description: "Wake up when watched agents go idle (exact state from Claude Code hooks) or max_wait_seconds passes - except delivery HOLDS past that bound instead, for as long as the target pane is on a dialog or has unsubmitted human text in it, rather than pasting the wake body into either (.claude/rules/tmux-and-panes.md). Two shapes, and you pass EXACTLY ONE of them. agents=[...] is a ONE-SHOT over a named list: mode=any fires on the first fresh idle transition, mode=all fires when every watched agent is idle (returns already_satisfied without scheduling anything if they all are now), and either way it stops watching once it fires. scope=\"project\" is a STANDING WATCH over the crew you spawn in this project, including workers spawned later: it never stops watching, and on each finish it delivers a roster naming who finished and who is still going, until max_wait_seconds runs out or you wake_cancel it. You may hold ONE standing watch per project: a second call is refused and names the one already running, since two would report every finish twice. Use the standing watch when you are running more than one worker - a one-shot leaves every other worker unwatched from the moment it fires. Use either instead of polling.
|
|
330
|
+
description: "Wake up when watched agents go idle (exact state from Claude Code hooks) or max_wait_seconds passes - except delivery HOLDS past that bound instead, for as long as the target pane is on a dialog or has unsubmitted human text in it, rather than pasting the wake body into either (.claude/rules/tmux-and-panes.md). Two shapes for workers, plus one for the queen, and you pass EXACTLY ONE of them. agents=[...] is a ONE-SHOT over a named list: mode=any fires on the first fresh idle transition, mode=all fires when every watched agent is idle (returns already_satisfied without scheduling anything if they all are now), and either way it stops watching once it fires. scope=\"project\" is a STANDING WATCH over the crew you spawn in this project, including workers spawned later: it never stops watching, and on each finish it delivers a roster naming who finished and who is still going, until max_wait_seconds runs out or you wake_cancel it. You may hold ONE standing watch per project: a second call is refused and names the one already running, since two would report every finish twice. Use the standing watch when you are running more than one worker - a one-shot leaves every other worker unwatched from the moment it fires. Use either instead of polling. agents=[...] refuses a lead: it watches worker state, which a lead does not write. lead_project_id is the QUEEN's alone: a one-shot that fires when another registered project's running lead ENDS A TURN (never 'finished its work'), stored in and delivered to the queen's own project, and ended with a named reason if that lead's pane dies, is reissued, or restarts.",
|
|
219
331
|
annotations: {
|
|
220
332
|
readOnlyHint: false,
|
|
221
333
|
destructiveHint: false,
|
|
@@ -244,11 +356,17 @@ export function registerWakes(server) {
|
|
|
244
356
|
"scope=\"project\": THE WATCH'S LIFETIME, default 14400 (4 hours), after which it delivers one " +
|
|
245
357
|
"last wake saying it has expired and stops watching. Not a hard deadline either way: delivery " +
|
|
246
358
|
"holds past it while the target pane is on a dialog or has unsubmitted text, until the pane clears."),
|
|
359
|
+
lead_project_id: idParam
|
|
360
|
+
.optional()
|
|
361
|
+
.describe("QUEEN ONLY: wake when the running lead of this OTHER registered project ends a turn. The wake is " +
|
|
362
|
+
"stored in the queen's own project and delivered only to the queen; the watched lead is read, never " +
|
|
363
|
+
"typed into. Mutually exclusive with agents and scope; refuses project_id and deliver_to."),
|
|
247
364
|
deliver_to: agentRefParam.optional().describe("Deliver to a spawned agent instead of this session."),
|
|
248
365
|
project_id: projectIdParam,
|
|
249
366
|
},
|
|
250
367
|
outputSchema: {
|
|
251
368
|
status: z.literal("already_satisfied").optional(),
|
|
369
|
+
lead_watch: z.record(z.string(), z.unknown()).optional(),
|
|
252
370
|
wake_id: idParam.optional(),
|
|
253
371
|
scope: z.literal("project").optional(),
|
|
254
372
|
standing: z.boolean().optional(),
|
|
@@ -270,13 +388,18 @@ export function registerWakes(server) {
|
|
|
270
388
|
}, (args) => run(() => {
|
|
271
389
|
rejectUnsafeBody(args.body);
|
|
272
390
|
const projectId = effectiveProjectId(args.project_id);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
391
|
+
const selectors = [
|
|
392
|
+
args.agents != null && "agents",
|
|
393
|
+
args.scope != null && "scope",
|
|
394
|
+
args.lead_project_id != null && "lead_project_id",
|
|
395
|
+
].filter((x) => x !== false);
|
|
396
|
+
if (selectors.length !== 1) {
|
|
397
|
+
throw new Error("wake_when_idle needs exactly one of agents=[...] (a one-shot over a named list), " +
|
|
398
|
+
'scope="project" (a standing watch over this project\'s crew), or lead_project_id (the queen ' +
|
|
399
|
+
"waiting on another project's lead). " +
|
|
400
|
+
(selectors.length === 0
|
|
401
|
+
? "You passed none."
|
|
402
|
+
: `You passed ${selectors.length === 2 ? "both" : "all of"} ${selectors.join(" and ")}, and they mean different things.`));
|
|
280
403
|
}
|
|
281
404
|
if (args.scope != null && args.mode != null) {
|
|
282
405
|
throw new Error(`mode="${args.mode}" has no meaning for a standing watch: a standing watch reports EACH crew ` +
|
|
@@ -285,14 +408,17 @@ export function registerWakes(server) {
|
|
|
285
408
|
}
|
|
286
409
|
if (args.scope != null)
|
|
287
410
|
return createStandingWatch(projectId, args);
|
|
411
|
+
if (args.lead_project_id != null)
|
|
412
|
+
return createLeadWatch(args.lead_project_id, args);
|
|
288
413
|
const mode = args.mode ?? "any";
|
|
289
414
|
const watched = (args.agents ?? []).map((ref) => resolveAgentRef(projectId, ref));
|
|
290
415
|
const leadWatched = watched.find((a) => a.kind === LEAD_KIND);
|
|
291
416
|
if (leadWatched) {
|
|
292
|
-
throw new Error(`Agent ${leadWatched.id} ("${leadWatched.name}") is this project's lead session,
|
|
293
|
-
"
|
|
294
|
-
"wake_when_idle refuses a lead target; it could only ever fire at max_wait_seconds,
|
|
295
|
-
"as a timeout instead of the failure it actually is."
|
|
417
|
+
throw new Error(`Agent ${leadWatched.id} ("${leadWatched.name}") is this project's lead session, and agents=[...] ` +
|
|
418
|
+
"watches worker state, which a lead does not write: its hook never moves agent_state. " +
|
|
419
|
+
"wake_when_idle refuses a lead target in agents=[...]; it could only ever fire at max_wait_seconds, " +
|
|
420
|
+
"reported as a timeout instead of the failure it actually is. A lead's turn has its own channel, " +
|
|
421
|
+
"which only the queen may watch, with lead_project_id=<that lead's project id>.");
|
|
296
422
|
}
|
|
297
423
|
const delivery = resolveDelivery(projectId, args.deliver_to);
|
|
298
424
|
const snapshot = liveTargets();
|
|
@@ -311,24 +437,26 @@ export function registerWakes(server) {
|
|
|
311
437
|
}
|
|
312
438
|
}
|
|
313
439
|
const maxWait = args.max_wait_seconds ?? 900;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
440
|
+
return commitQueenStateWrite("wake_when_idle", projectId, args, () => {
|
|
441
|
+
const info = db
|
|
442
|
+
.prepare(`INSERT INTO wakes (project_id, owner, body, kind, watch, deliver_actor, deliver_pane,
|
|
443
|
+
max_wait_at)
|
|
444
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now', printf('+%d seconds', ?)))`)
|
|
445
|
+
.run(projectId, currentActor(), args.body, `idle_${mode}`, JSON.stringify(watched.map((a) => a.id)), delivery.actor, delivery.pane, maxWait);
|
|
446
|
+
return {
|
|
447
|
+
wake_id: Number(info.lastInsertRowid),
|
|
448
|
+
mode,
|
|
449
|
+
watching: watched.map((a) => {
|
|
450
|
+
const { state, ...provenance } = deriveProvenance(a, summaryLiveness(a, snapshot));
|
|
451
|
+
return { agent_id: a.id, name: a.name, state, provenance };
|
|
452
|
+
}),
|
|
453
|
+
max_wait_seconds: maxWait,
|
|
454
|
+
deliver_to: delivery.actor,
|
|
455
|
+
note: mode === "any"
|
|
456
|
+
? "Fires on the next fresh idle transition; agents already idle now do not count."
|
|
457
|
+
: "Fires when all watched agents are idle.",
|
|
458
|
+
};
|
|
459
|
+
});
|
|
332
460
|
}));
|
|
333
461
|
server.registerTool("wake_get", {
|
|
334
462
|
description: "Read one wake-up by id, in this project, with its UNTRUNCATED body. wake_list truncates " +
|
|
@@ -416,12 +544,14 @@ export function registerWakes(server) {
|
|
|
416
544
|
params.push(args.delay_seconds);
|
|
417
545
|
}
|
|
418
546
|
params.push(args.wake_id, projectId, currentActor());
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
547
|
+
return commitQueenStateWrite("wake_update", projectId, args, () => {
|
|
548
|
+
const row = db
|
|
549
|
+
.prepare(`UPDATE wakes SET ${sets.join(", ")}
|
|
550
|
+
WHERE id = ? AND project_id = ? AND owner = ? AND parent_wake_id IS NULL AND ${ACTIVE_TIMER_WHERE}
|
|
551
|
+
RETURNING due_at`)
|
|
552
|
+
.get(...params);
|
|
553
|
+
return { wake_id: args.wake_id, updated: row !== undefined, due_at: row?.due_at ?? null };
|
|
554
|
+
});
|
|
425
555
|
}));
|
|
426
556
|
server.registerTool("wake_cancel", {
|
|
427
557
|
description: "Cancel a pending wake-up you own, or - if you are a running lead - any pending wake-up in this " +
|
|
@@ -448,22 +578,24 @@ export function registerWakes(server) {
|
|
|
448
578
|
}, (args) => run(() => {
|
|
449
579
|
const projectId = effectiveProjectId(args.project_id);
|
|
450
580
|
const isLead = isRunningLeadActor(currentActor());
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
581
|
+
return commitQueenStateWrite("wake_cancel", projectId, args, () => {
|
|
582
|
+
const info = isLead
|
|
583
|
+
? db
|
|
584
|
+
.prepare(`UPDATE wakes SET cancelled_at = datetime('now')
|
|
585
|
+
WHERE id = ? AND project_id = ? AND cancelled_at IS NULL`)
|
|
586
|
+
.run(args.wake_id, projectId)
|
|
587
|
+
: db
|
|
588
|
+
.prepare(`UPDATE wakes SET cancelled_at = datetime('now')
|
|
589
|
+
WHERE id = ? AND project_id = ? AND owner = ? AND cancelled_at IS NULL`)
|
|
590
|
+
.run(args.wake_id, projectId, currentActor());
|
|
591
|
+
const notices = info.changes > 0
|
|
592
|
+
? db
|
|
593
|
+
.prepare(`UPDATE wakes SET cancelled_at = datetime('now')
|
|
594
|
+
WHERE parent_wake_id = ? AND cancelled_at IS NULL AND fired_at IS NULL`)
|
|
595
|
+
.run(args.wake_id).changes
|
|
596
|
+
: 0;
|
|
597
|
+
return { wake_id: args.wake_id, cancelled: info.changes > 0, cancelled_notices: notices };
|
|
598
|
+
});
|
|
467
599
|
}));
|
|
468
600
|
server.registerTool("wake_list", {
|
|
469
601
|
description: "List pending wake-ups in this project, plus recently_delivered: the last " +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmgmyr/hive",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Run a crew of Claude Code and Codex workers from one lead session: visible tmux panes, exact state from hooks, one local SQLite store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,15 +47,15 @@
|
|
|
47
47
|
"coordination"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@modelcontextprotocol/sdk": "^1.30.
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
51
51
|
"better-sqlite3": "^13.0.3",
|
|
52
|
-
"smol-toml": "^1.
|
|
52
|
+
"smol-toml": "^1.9.0",
|
|
53
53
|
"yaml": "^2.9.1",
|
|
54
54
|
"zod": "^4.6.5"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/better-sqlite3": "^9.6.0",
|
|
58
|
-
"@types/node": "^26.6.
|
|
58
|
+
"@types/node": "^26.6.3",
|
|
59
59
|
"typescript": "^7.0.2"
|
|
60
60
|
},
|
|
61
61
|
"allowScripts": {
|
|
@@ -7,7 +7,7 @@ Coordinate through the hive MCP tools:
|
|
|
7
7
|
- pad_list / pad_read for the shared plan and findings. Record decisions there.
|
|
8
8
|
- todo_list(is_blocked=false, status="open") for dispatchable work; set status to in_progress while working.
|
|
9
9
|
- todo_comment for handoffs (changed files, tests run, remaining risk), then todo_complete.
|
|
10
|
-
- lease_acquire before
|
|
10
|
+
- lease_acquire before changing state another session can also change (a shared checkout, a dev database, a port); files in a directory only you work in need none. Leases expire on their own.
|
|
11
11
|
<!--if:primary_root-->
|
|
12
12
|
The primary checkout is at {{primary_root}} - untracked project files (a session corpus, local notes) live there, not in your worktree, if this project keeps any.
|
|
13
13
|
<!--end-->
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
You are the queen: the lead of hive's own queen home, started by `hive queen`.
|
|
2
|
+
Every other lead owns one project. You watch all of them and steer; you do not
|
|
3
|
+
do their work.
|
|
4
|
+
|
|
5
|
+
- Read any registered project. `hive portfolio` gives one row per project;
|
|
6
|
+
pad_read, todo_list, todo_get and agent_status with that project's
|
|
7
|
+
project_id give the detail. Read fresh before you answer: a row you read an
|
|
8
|
+
hour ago is not the state now.
|
|
9
|
+
- Always pass project_id to reach another project; never project_select,
|
|
10
|
+
which hive refuses for the queen.
|
|
11
|
+
- Write into another project only through its lead. hive lets you do exactly
|
|
12
|
+
this there, and refuses the rest with QUEEN_CROSS_PROJECT_WRITE_REFUSED:
|
|
13
|
+
- todo_create and todo_comment, to hand work or a question to that project;
|
|
14
|
+
- wake_set and wake_when_idle with deliver_to set to that project's running
|
|
15
|
+
lead, and wake_update or wake_cancel on a wake of yours addressed to it;
|
|
16
|
+
- wake_when_idle with lead_project_id set to a project's id, to be woken
|
|
17
|
+
when that project's lead ends a turn;
|
|
18
|
+
- agent_send text (never keys) to that project's running lead;
|
|
19
|
+
- `hive lead <path>` to start a project's lead when it has none.
|
|
20
|
+
Pads, todo status, kv, leases and workers in another project belong to its
|
|
21
|
+
lead. Ask; do not reach in.
|
|
22
|
+
- queen_audit_list (or `hive queen-audit`) lists your confirmed writes into
|
|
23
|
+
other projects, newest first.
|
|
24
|
+
- Your own home is an ordinary project. Pads, todos and workers here are
|
|
25
|
+
yours, and workers you spawn stay locked to it.
|
|
26
|
+
- Carry a short label with any id you show anyone, and name its project:
|
|
27
|
+
"todo 318 in api (give todos a slug)", not "todo 318". Your reader is
|
|
28
|
+
rarely standing in that project.
|
|
29
|
+
- Anything outward-facing (pushes, published PRs, anything that leaves the
|
|
30
|
+
machine) waits for explicit human approval.
|
|
31
|
+
- When the human asks how work runs here, read `hive runbook`.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
RUNBOOK - how the queen works. This is hive's skeleton, not your process.
|
|
2
|
+
Fork it and fill it in:
|
|
3
|
+
|
|
4
|
+
hive profile fork queen runbook.md
|
|
5
|
+
|
|
6
|
+
Angle brackets are yours to replace.
|
|
7
|
+
|
|
8
|
+
WHAT THE QUEEN IS FOR
|
|
9
|
+
<the questions you bring to the queen rather than to a project's lead>
|
|
10
|
+
|
|
11
|
+
READING THE PORTFOLIO
|
|
12
|
+
Start from `hive portfolio`. It is deterministic and read-only: lead state,
|
|
13
|
+
workers by state, todo counts, needs-human todos and wakes, per project.
|
|
14
|
+
Open a project's pads and todos by project_id only when a row needs it.
|
|
15
|
+
|
|
16
|
+
ROUTING WORK
|
|
17
|
+
A finding about another project becomes a todo in that project
|
|
18
|
+
(todo_create with its project_id), or a comment on the todo it concerns.
|
|
19
|
+
Tell its lead with agent_send text or a wake addressed to that lead. The lead
|
|
20
|
+
decides what happens next; the queen does not edit its board.
|
|
21
|
+
|
|
22
|
+
STARTING A LEAD
|
|
23
|
+
<when a project with no running lead should get one: hive lead <path>>
|
|
24
|
+
|
|
25
|
+
ON A SCHEDULE
|
|
26
|
+
<the check-ins you want, set with wake_set on your own pane>
|