@cmgmyr/hive 1.10.1 → 1.11.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/claude-plugin/crew/model.mjs +2 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +125 -46
- package/dist/context.js +7 -1
- package/dist/crew.js +20 -2
- package/dist/db.js +139 -0
- package/dist/help.js +27 -0
- package/dist/hook.js +32 -2
- package/dist/index.js +2 -0
- package/dist/kickoff.js +29 -6
- package/dist/leadHandoff.js +525 -0
- package/dist/leadHandoffCli.js +295 -0
- package/dist/leadHandoffState.js +96 -0
- package/dist/leadLaunch.js +54 -0
- package/dist/leadMessageDelivery.js +44 -7
- package/dist/leadState.js +98 -17
- package/dist/portfolio.js +2 -0
- package/dist/projectRemove.js +1 -0
- package/dist/projectYml.js +8 -1
- package/dist/questions.js +254 -0
- package/dist/scheduler.js +25 -5
- package/dist/staleServers.js +122 -0
- package/dist/tools/meta.js +4 -1
- package/dist/tools/questions.js +87 -0
- package/dist/tools/wakes.js +9 -9
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { currentActor, effectiveProjectId } from "../context.js";
|
|
3
|
+
import { run } from "../result.js";
|
|
4
|
+
import { answerQuestion, askQuestion, getQuestion, listQuestions, updateQuestion, } from "../questions.js";
|
|
5
|
+
import { idParam } from "./params.js";
|
|
6
|
+
const optionParam = z.strictObject({
|
|
7
|
+
label: z.string().describe("Short choice label; distinct within the question."),
|
|
8
|
+
description: z.string().describe("What choosing this means."),
|
|
9
|
+
recommended: z.boolean().describe("Exactly one option is recommended; it is the default."),
|
|
10
|
+
});
|
|
11
|
+
const refParam = z.strictObject({
|
|
12
|
+
kind: z.enum(["todo", "pr"]),
|
|
13
|
+
number: idParam.describe("A todo id or PR number. An identifier, not looked up."),
|
|
14
|
+
});
|
|
15
|
+
const stateParam = z.enum(["answered", "defaulted", "pending", "withdrawn"]);
|
|
16
|
+
const noWrites = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
17
|
+
const writes = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
18
|
+
export function registerQuestions(server) {
|
|
19
|
+
server.registerTool("question_ask", {
|
|
20
|
+
description: "Record an open question for the human, keyed to this project and to you as the asker. kind=choice takes 2-4 options (each label, description, recommended; exactly one recommended) and a header of 12 characters or fewer; kind=free_text takes just the question and header. Every question carries default_answer: for a choice it is the recommended option's label. Pass ref (a todo id or PR number) to dedupe: asking again with a ref that already has a PENDING question in this project updates that row instead of adding one. The receipt lists other pending questions in this project that look similar (same normalized text, or at least half their longer words shared), so you can question_update one instead. The record is only stored: put the question to the human yourself, in your own turn. You must be a session an answer can be woken back to. When the question is answered, an ordinary wake carrying the answer is queued to you; read the record with question_get.",
|
|
21
|
+
annotations: writes,
|
|
22
|
+
inputSchema: {
|
|
23
|
+
kind: z.enum(["choice", "free_text"]),
|
|
24
|
+
question: z.string(),
|
|
25
|
+
header: z.string().describe("12 characters or fewer."),
|
|
26
|
+
options: z.array(optionParam).optional().describe("Required for choice, forbidden for free_text."),
|
|
27
|
+
default_answer: z.string().describe("What you do if unanswered. For a choice, the recommended label."),
|
|
28
|
+
ref: refParam.optional(),
|
|
29
|
+
},
|
|
30
|
+
outputSchema: {
|
|
31
|
+
question_id: idParam,
|
|
32
|
+
created: z.boolean(),
|
|
33
|
+
similar_question_ids: z.array(idParam),
|
|
34
|
+
},
|
|
35
|
+
}, (args) => run(() => {
|
|
36
|
+
const projectId = effectiveProjectId();
|
|
37
|
+
const input = {
|
|
38
|
+
kind: args.kind,
|
|
39
|
+
question: args.question,
|
|
40
|
+
header: args.header,
|
|
41
|
+
default_answer: args.default_answer,
|
|
42
|
+
...(args.options !== undefined ? { options: args.options } : {}),
|
|
43
|
+
...(args.ref !== undefined ? { ref: args.ref } : {}),
|
|
44
|
+
};
|
|
45
|
+
return askQuestion(projectId, currentActor(), input);
|
|
46
|
+
}));
|
|
47
|
+
server.registerTool("question_get", {
|
|
48
|
+
description: "Read one question in this project by id, in any state, with its answer and notes once answered.",
|
|
49
|
+
annotations: noWrites,
|
|
50
|
+
inputSchema: { question_id: idParam },
|
|
51
|
+
}, (args) => run(() => getQuestion(effectiveProjectId(), args.question_id)));
|
|
52
|
+
server.registerTool("question_list", {
|
|
53
|
+
description: "List this project's pending questions, then its defaulted ones (acted on by default, still open to reversal), oldest first within each group. Answered and withdrawn questions are not listed; read one by id with question_get.",
|
|
54
|
+
annotations: noWrites,
|
|
55
|
+
inputSchema: {},
|
|
56
|
+
}, () => run(() => ({ questions: listQuestions(effectiveProjectId()) })));
|
|
57
|
+
server.registerTool("question_update", {
|
|
58
|
+
description: "Edit a pending question's text (question, header, options, default_answer; the whole result is revalidated) or move it out of pending with state=defaulted (you acted on your default) or state=withdrawn (moot). A defaulted question can still be withdrawn. Moving state needs the asking session or the human; an answered or withdrawn question cannot be changed. The ref, asker and wake target never change.",
|
|
59
|
+
annotations: writes,
|
|
60
|
+
inputSchema: {
|
|
61
|
+
question_id: idParam,
|
|
62
|
+
question: z.string().optional(),
|
|
63
|
+
header: z.string().optional(),
|
|
64
|
+
options: z.array(optionParam).optional(),
|
|
65
|
+
default_answer: z.string().optional(),
|
|
66
|
+
state: z.enum(["defaulted", "withdrawn"]).optional(),
|
|
67
|
+
},
|
|
68
|
+
outputSchema: { question_id: idParam, state: stateParam },
|
|
69
|
+
}, (args) => run(() => {
|
|
70
|
+
const { question_id, ...patch } = args;
|
|
71
|
+
return updateQuestion(effectiveProjectId(), currentActor(), question_id, patch);
|
|
72
|
+
}));
|
|
73
|
+
server.registerTool("question_answer", {
|
|
74
|
+
description: "Record the human's answer to a pending or defaulted question in this project: a picked option label or free text (any nonblank text is accepted, including one outside the offered labels), plus optional notes. The answer commits first. It then queues one wake to the asking session carrying the question, answer and notes verbatim, unless you ARE the asker (you recorded the human's own pick) or the asker cannot be reached now; either way the receipt says wake_id null and why in wake_skipped, and the answer stays readable with question_get. wake_id means queued, not delivered. An answered or withdrawn question refuses with QUESTION_STATE_CONFLICT.",
|
|
75
|
+
annotations: writes,
|
|
76
|
+
inputSchema: {
|
|
77
|
+
question_id: idParam,
|
|
78
|
+
answer: z.string(),
|
|
79
|
+
notes: z.string().optional(),
|
|
80
|
+
},
|
|
81
|
+
outputSchema: {
|
|
82
|
+
question_id: idParam,
|
|
83
|
+
wake_id: idParam.nullable(),
|
|
84
|
+
wake_skipped: z.string().optional(),
|
|
85
|
+
},
|
|
86
|
+
}, (args) => run(() => answerQuestion(effectiveProjectId(), currentActor(), args.question_id, args.answer, args.notes)));
|
|
87
|
+
}
|
package/dist/tools/wakes.js
CHANGED
|
@@ -35,7 +35,7 @@ function refuseUnclassifiableTarget(agent) {
|
|
|
35
35
|
"error you can read now. Drive this pane with agent_send(keys: [...]) instead, which is unguarded for " +
|
|
36
36
|
"exactly this reason.");
|
|
37
37
|
}
|
|
38
|
-
function
|
|
38
|
+
export function resolveWakeDelivery(projectId, deliverTo) {
|
|
39
39
|
if (deliverTo != null) {
|
|
40
40
|
const agent = resolveAgentRef(projectId, deliverTo);
|
|
41
41
|
// Before the liveness probe: the harness is a durable fact about the row, while liveness is a
|
|
@@ -130,7 +130,7 @@ function deliveredByNote(t) {
|
|
|
130
130
|
: null;
|
|
131
131
|
}
|
|
132
132
|
export const truncateBody = (body) => body.length > 120 ? `${cutToUnitBudget(body, 120)}…` : body;
|
|
133
|
-
function
|
|
133
|
+
export function assertSafeWakeBody(body) {
|
|
134
134
|
const bad = findUnsafeControlChar(body, TEXT_ALLOWED_CONTROL_CHARS);
|
|
135
135
|
if (bad) {
|
|
136
136
|
throw new Error(`body cannot contain ${bad.label} at offset ${bad.index}: it is typed literally into the target pane ` +
|
|
@@ -173,7 +173,7 @@ const openStandingWatch = db.transaction((projectId, owner, body, deliverActor,
|
|
|
173
173
|
return row;
|
|
174
174
|
});
|
|
175
175
|
function createStandingWatch(projectId, args) {
|
|
176
|
-
const delivery =
|
|
176
|
+
const delivery = resolveWakeDelivery(projectId, args.deliver_to);
|
|
177
177
|
const maxWait = args.max_wait_seconds ?? STANDING_WATCH_LIFETIME_SECONDS;
|
|
178
178
|
const row = openStandingWatch.immediate(projectId, currentActor(), args.body, delivery.actor, delivery.pane, maxWait);
|
|
179
179
|
const crew = db
|
|
@@ -261,7 +261,7 @@ function createLeadWatch(leadProjectId, args) {
|
|
|
261
261
|
note: `Project ${leadProjectId}'s lead has already ended its turn; nothing was scheduled. Read it now.`,
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
|
-
const delivery =
|
|
264
|
+
const delivery = resolveWakeDelivery(home);
|
|
265
265
|
const maxWait = args.max_wait_seconds ?? 900;
|
|
266
266
|
const row = openLeadWatch.immediate(home, args.body, mode, delivery, maxWait, {
|
|
267
267
|
projectId: leadProjectId,
|
|
@@ -329,9 +329,9 @@ export function registerWakes(server) {
|
|
|
329
329
|
repeating: z.boolean(),
|
|
330
330
|
},
|
|
331
331
|
}, (args) => run(() => {
|
|
332
|
-
|
|
332
|
+
assertSafeWakeBody(args.body);
|
|
333
333
|
const projectId = effectiveProjectId(args.project_id);
|
|
334
|
-
const delivery =
|
|
334
|
+
const delivery = resolveWakeDelivery(projectId, args.deliver_to);
|
|
335
335
|
return commitQueenStateWrite("wake_set", projectId, args, () => {
|
|
336
336
|
const row = db
|
|
337
337
|
.prepare(`INSERT INTO wakes (project_id, owner, body, kind, deliver_actor, deliver_pane, due_at, repeat_every_ms)
|
|
@@ -407,7 +407,7 @@ export function registerWakes(server) {
|
|
|
407
407
|
note: z.string().optional(),
|
|
408
408
|
},
|
|
409
409
|
}, (args) => run(() => {
|
|
410
|
-
|
|
410
|
+
assertSafeWakeBody(args.body);
|
|
411
411
|
const projectId = effectiveProjectId(args.project_id);
|
|
412
412
|
const selectors = [
|
|
413
413
|
args.agents != null && "agents",
|
|
@@ -441,7 +441,7 @@ export function registerWakes(server) {
|
|
|
441
441
|
"reported as a timeout instead of the failure it actually is. A lead's turn has its own channel, " +
|
|
442
442
|
"which only the queen may watch, with lead_project_id=<that lead's project id>.");
|
|
443
443
|
}
|
|
444
|
-
const delivery =
|
|
444
|
+
const delivery = resolveWakeDelivery(projectId, args.deliver_to);
|
|
445
445
|
const snapshot = liveTargets();
|
|
446
446
|
if (mode === "all") {
|
|
447
447
|
const allIdle = watched.every((a) => {
|
|
@@ -577,7 +577,7 @@ export function registerWakes(server) {
|
|
|
577
577
|
const sets = [];
|
|
578
578
|
const params = [];
|
|
579
579
|
if (args.body != null) {
|
|
580
|
-
|
|
580
|
+
assertSafeWakeBody(args.body);
|
|
581
581
|
sets.push("body = ?");
|
|
582
582
|
params.push(args.body);
|
|
583
583
|
}
|
package/package.json
CHANGED