@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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { db } from "./db.js";
|
|
2
|
+
import { paneClassifierFor } from "./harnesses.js";
|
|
3
|
+
import { readLeadTurnState } from "./leadState.js";
|
|
4
|
+
import { LEAD_KIND } from "./spawn.js";
|
|
5
|
+
import { holdsHumanInput, paneReissued, rowAliveProbe } from "./tmux.js";
|
|
6
|
+
export const LEAD_TARGET_GONE = "LEAD_TARGET_GONE";
|
|
7
|
+
export const LEAD_TARGET_REISSUED = "LEAD_TARGET_REISSUED";
|
|
8
|
+
export const LEAD_TARGET_RESTARTED = "LEAD_TARGET_RESTARTED";
|
|
9
|
+
const directPaneReaders = {
|
|
10
|
+
awaitingChoice: (pane, command) => paneClassifierFor(command)?.choiceCheck(pane).awaitingChoice ?? null,
|
|
11
|
+
holdsInput: (pane, command) => holdsHumanInput(paneClassifierFor(command)?.inputBoxState(pane) ?? null),
|
|
12
|
+
};
|
|
13
|
+
export function leadSubscription(wakeId) {
|
|
14
|
+
return (db.prepare("SELECT * FROM lead_idle_subscriptions WHERE wake_id = ?").get(wakeId) ??
|
|
15
|
+
null);
|
|
16
|
+
}
|
|
17
|
+
export function runningLeadOf(projectId) {
|
|
18
|
+
return (db
|
|
19
|
+
.prepare(`SELECT id, name, status, tmux_target, tmux_socket, pane_pid, command FROM agents
|
|
20
|
+
WHERE project_id = ? AND kind = ? AND status = 'running' ORDER BY id LIMIT 1`)
|
|
21
|
+
.get(projectId, LEAD_KIND) ?? null);
|
|
22
|
+
}
|
|
23
|
+
// Reads the watched lead's screen only to stay quiet; it can never make a lead read idle.
|
|
24
|
+
export function paneVeto(lead, readers = directPaneReaders) {
|
|
25
|
+
const choice = readers.awaitingChoice(lead.tmux_target, lead.command);
|
|
26
|
+
if (choice === true)
|
|
27
|
+
return "it is on a dialog";
|
|
28
|
+
if (choice === null)
|
|
29
|
+
return "its screen could not be read";
|
|
30
|
+
if (readers.holdsInput(lead.tmux_target, lead.command))
|
|
31
|
+
return "it has unsubmitted text in its input box";
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export function evaluateLeadWatch(sub, mode, snapshot, readers) {
|
|
35
|
+
const lead = db
|
|
36
|
+
.prepare("SELECT id, name, status, tmux_target, tmux_socket, pane_pid, command FROM agents WHERE id = ? AND kind = ?")
|
|
37
|
+
.get(sub.agent_id, LEAD_KIND);
|
|
38
|
+
if (!lead || lead.status !== "running")
|
|
39
|
+
return { kind: "invalid", reason: LEAD_TARGET_GONE };
|
|
40
|
+
if (lead.pane_pid !== sub.pane_pid)
|
|
41
|
+
return { kind: "invalid", reason: LEAD_TARGET_RESTARTED };
|
|
42
|
+
if (snapshot === null)
|
|
43
|
+
return { kind: "pending", reason: "tmux did not answer" };
|
|
44
|
+
const probe = rowAliveProbe(lead.tmux_socket, lead.tmux_target, snapshot);
|
|
45
|
+
if (probe.live === null)
|
|
46
|
+
return { kind: "pending", reason: "its pane cannot be probed from this process" };
|
|
47
|
+
if (!probe.live)
|
|
48
|
+
return { kind: "invalid", reason: LEAD_TARGET_GONE };
|
|
49
|
+
if (paneReissued(sub.pane_pid, probe))
|
|
50
|
+
return { kind: "invalid", reason: LEAD_TARGET_REISSUED };
|
|
51
|
+
const state = readLeadTurnState(lead.id);
|
|
52
|
+
if (state === null || state.pane_pid !== sub.pane_pid)
|
|
53
|
+
return { kind: "pending", reason: "no turn recorded yet" };
|
|
54
|
+
let session = sub.session_id;
|
|
55
|
+
if (session === "" && state.state !== "unknown") {
|
|
56
|
+
session = state.session_id;
|
|
57
|
+
try {
|
|
58
|
+
db.prepare("UPDATE lead_idle_subscriptions SET session_id = ? WHERE wake_id = ? AND session_id = ''").run(session, sub.wake_id);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Unbound for another tick; the in-memory binding still judges this one.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (session !== "" && state.session_id !== session)
|
|
65
|
+
return { kind: "invalid", reason: LEAD_TARGET_RESTARTED };
|
|
66
|
+
const ended = mode === "any" ? state.idle_seq > sub.baseline_idle_seq : state.state === "idle";
|
|
67
|
+
if (!ended)
|
|
68
|
+
return { kind: "pending", reason: `its turn state is ${state.state}` };
|
|
69
|
+
const vetoed = paneVeto(lead, readers);
|
|
70
|
+
if (vetoed !== null)
|
|
71
|
+
return { kind: "pending", reason: `its turn ended, but ${vetoed}` };
|
|
72
|
+
return { kind: "idle", idle_seq: state.idle_seq };
|
|
73
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { db } from "./db.js";
|
|
2
|
+
import { listProjects } from "./context.js";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { parseTags } from "./result.js";
|
|
5
|
+
import { OPEN_BLOCKERS_SQL } from "./tools/todos.js";
|
|
6
|
+
import { readLeadTurnState } from "./leadState.js";
|
|
7
|
+
import { liveTargets, paneReissued, rowAlive, rowAliveProbe } from "./tmux.js";
|
|
8
|
+
export const PORTFOLIO_REASONS = [
|
|
9
|
+
"needs_human",
|
|
10
|
+
"dead_lead_pane",
|
|
11
|
+
"missing_root_with_work",
|
|
12
|
+
"worker_needs_input",
|
|
13
|
+
"wake_overdue_5m",
|
|
14
|
+
"in_progress_blocked",
|
|
15
|
+
"all_active_todos_blocked",
|
|
16
|
+
"stale_in_progress_48h",
|
|
17
|
+
"worker_working",
|
|
18
|
+
"todo_in_progress",
|
|
19
|
+
"wake_due_24h",
|
|
20
|
+
"quiet",
|
|
21
|
+
];
|
|
22
|
+
export const NEEDS_HUMAN_TAG = "needs-human";
|
|
23
|
+
const NEEDS_HUMAN_ITEM_CAP = 5;
|
|
24
|
+
const WAKE_OVERDUE_GRACE = "-5 minutes";
|
|
25
|
+
const STALE_IN_PROGRESS = "-48 hours";
|
|
26
|
+
const WAKE_UPCOMING = "+24 hours";
|
|
27
|
+
export const ACTIVE_WAKE_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
|
|
28
|
+
export function leadText(p) {
|
|
29
|
+
return p.lead.state === "alive" && p.lead.turn !== "unknown"
|
|
30
|
+
? `alive, turn ${p.lead.turn === "working" ? "working" : "ended"}`
|
|
31
|
+
: p.lead.state;
|
|
32
|
+
}
|
|
33
|
+
function leadTurnFor(agentId, panePid) {
|
|
34
|
+
const row = readLeadTurnState(agentId);
|
|
35
|
+
if (row === null || row.pane_pid !== panePid)
|
|
36
|
+
return "unknown";
|
|
37
|
+
return row.state === "working" ? "working" : row.state === "idle" ? "turn_ended" : "unknown";
|
|
38
|
+
}
|
|
39
|
+
const ONE = (sql, ...params) => db.prepare(sql).get(...params);
|
|
40
|
+
function shiftedClock(now, modifier) {
|
|
41
|
+
return ONE("SELECT datetime(?, ?) AS t", now, modifier).t;
|
|
42
|
+
}
|
|
43
|
+
function lastActivity(projectId, countWakeFires = true) {
|
|
44
|
+
const row = ONE(`SELECT MAX(t) AS t FROM (
|
|
45
|
+
SELECT created_at AS t FROM projects WHERE id = :pid
|
|
46
|
+
UNION ALL SELECT updated_at FROM todos WHERE project_id = :pid
|
|
47
|
+
UNION ALL SELECT c.created_at FROM todo_comments c JOIN todos t ON t.id = c.todo_id WHERE t.project_id = :pid
|
|
48
|
+
UNION ALL SELECT updated_at FROM pads WHERE project_id = :pid
|
|
49
|
+
UNION ALL SELECT created_at FROM agents WHERE project_id = :pid
|
|
50
|
+
UNION ALL SELECT state_changed_at FROM agents WHERE project_id = :pid
|
|
51
|
+
UNION ALL SELECT substr(l.created_at, 1, 19) FROM agent_state_log l
|
|
52
|
+
WHERE l.actor_id IN (SELECT actor_id FROM agents WHERE project_id = :pid)
|
|
53
|
+
UNION ALL SELECT created_at FROM wakes WHERE project_id = :pid
|
|
54
|
+
${countWakeFires ? "UNION ALL SELECT fired_at FROM wakes WHERE project_id = :pid" : ""}
|
|
55
|
+
)`, { pid: projectId });
|
|
56
|
+
return row.t;
|
|
57
|
+
}
|
|
58
|
+
function projectRow(project, now, snapshot) {
|
|
59
|
+
const rootExists = existsSync(project.path);
|
|
60
|
+
const todoRows = db
|
|
61
|
+
.prepare(`SELECT t.id, t.title, t.slug, t.priority, t.status, t.tags, t.updated_at,
|
|
62
|
+
EXISTS (${OPEN_BLOCKERS_SQL}) AS blocked
|
|
63
|
+
FROM todos t
|
|
64
|
+
WHERE t.project_id = ? AND t.status IN ('open', 'in_progress') AND t.archived_at IS NULL`)
|
|
65
|
+
.all(project.id);
|
|
66
|
+
const todos = { open: 0, in_progress: 0, blocked: 0, blocked_in_progress: 0, high: 0 };
|
|
67
|
+
const tagged = [];
|
|
68
|
+
for (const t of todoRows) {
|
|
69
|
+
if (t.status === "open")
|
|
70
|
+
todos.open++;
|
|
71
|
+
else
|
|
72
|
+
todos.in_progress++;
|
|
73
|
+
if (t.blocked) {
|
|
74
|
+
todos.blocked++;
|
|
75
|
+
if (t.status === "in_progress")
|
|
76
|
+
todos.blocked_in_progress++;
|
|
77
|
+
}
|
|
78
|
+
if (t.priority === "high")
|
|
79
|
+
todos.high++;
|
|
80
|
+
if (parseTags(t.tags).includes(NEEDS_HUMAN_TAG))
|
|
81
|
+
tagged.push(t);
|
|
82
|
+
}
|
|
83
|
+
tagged.sort((a, b) => (a.updated_at < b.updated_at ? -1 : a.updated_at > b.updated_at ? 1 : a.id - b.id));
|
|
84
|
+
const agents = db
|
|
85
|
+
.prepare("SELECT id, kind, agent_state, tmux_target, tmux_socket, pane_pid FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
|
|
86
|
+
.all(project.id);
|
|
87
|
+
const liveness = (a) => snapshot ? rowAlive(a.tmux_socket, a.tmux_target, snapshot) : null;
|
|
88
|
+
const reissued = (a) => snapshot !== null && paneReissued(a.pane_pid, rowAliveProbe(a.tmux_socket, a.tmux_target, snapshot));
|
|
89
|
+
const workers = { working: 0, idle: 0, needs_input: 0, other: 0, unreachable: 0, unconfirmed: 0 };
|
|
90
|
+
let liveWorking = 0;
|
|
91
|
+
let liveWaiting = 0;
|
|
92
|
+
let lead = { state: "none", agent_id: null, turn: "unknown" };
|
|
93
|
+
for (const a of agents) {
|
|
94
|
+
if (a.kind === "lead") {
|
|
95
|
+
const live = reissued(a) ? false : liveness(a);
|
|
96
|
+
lead = {
|
|
97
|
+
state: live === true ? "alive" : live === false ? "dead_pane" : "unknown",
|
|
98
|
+
agent_id: a.id,
|
|
99
|
+
turn: live === true ? leadTurnFor(a.id, a.pane_pid) : "unknown",
|
|
100
|
+
};
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (a.kind !== "agent")
|
|
104
|
+
continue;
|
|
105
|
+
const live = reissued(a) ? false : liveness(a);
|
|
106
|
+
if (live === false)
|
|
107
|
+
workers.unreachable++;
|
|
108
|
+
else if (live === null)
|
|
109
|
+
workers.unconfirmed++;
|
|
110
|
+
else if (a.agent_state === "working") {
|
|
111
|
+
workers.working++;
|
|
112
|
+
liveWorking++;
|
|
113
|
+
}
|
|
114
|
+
else if (a.agent_state === "idle")
|
|
115
|
+
workers.idle++;
|
|
116
|
+
else if (a.agent_state === "waiting") {
|
|
117
|
+
workers.needs_input++;
|
|
118
|
+
liveWaiting++;
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
workers.other++;
|
|
122
|
+
}
|
|
123
|
+
const overdueBefore = shiftedClock(now, WAKE_OVERDUE_GRACE);
|
|
124
|
+
const upcomingUntil = shiftedClock(now, WAKE_UPCOMING);
|
|
125
|
+
const wakeCounts = db
|
|
126
|
+
.prepare(`SELECT COUNT(*) AS pending,
|
|
127
|
+
COALESCE(SUM(held_at IS NULL AND COALESCE(due_at, max_wait_at) < ?), 0) AS overdue,
|
|
128
|
+
COALESCE(SUM(held_at IS NULL AND COALESCE(due_at, max_wait_at) <= ?), 0) AS overdue_grace,
|
|
129
|
+
COALESCE(SUM(COALESCE(due_at, max_wait_at) > ? AND COALESCE(due_at, max_wait_at) <= ?), 0) AS upcoming
|
|
130
|
+
FROM wakes WHERE project_id = ? AND ${ACTIVE_WAKE_WHERE}`)
|
|
131
|
+
.get(now, overdueBefore, overdueBefore, upcomingUntil, project.id);
|
|
132
|
+
const lastActivityAt = lastActivity(project.id);
|
|
133
|
+
const staleClock = lastActivity(project.id, false);
|
|
134
|
+
const staleBefore = shiftedClock(now, STALE_IN_PROGRESS);
|
|
135
|
+
const activeTodos = todos.open + todos.in_progress;
|
|
136
|
+
const found = new Set();
|
|
137
|
+
if (tagged.length > 0)
|
|
138
|
+
found.add("needs_human");
|
|
139
|
+
if (lead.state === "dead_pane" && (todos.in_progress > 0 || wakeCounts.pending > 0))
|
|
140
|
+
found.add("dead_lead_pane");
|
|
141
|
+
if (!rootExists && (activeTodos > 0 || wakeCounts.pending > 0))
|
|
142
|
+
found.add("missing_root_with_work");
|
|
143
|
+
if (liveWaiting > 0)
|
|
144
|
+
found.add("worker_needs_input");
|
|
145
|
+
if (wakeCounts.overdue_grace > 0)
|
|
146
|
+
found.add("wake_overdue_5m");
|
|
147
|
+
if (todos.blocked_in_progress > 0)
|
|
148
|
+
found.add("in_progress_blocked");
|
|
149
|
+
if (activeTodos > 0 && todos.blocked === activeTodos)
|
|
150
|
+
found.add("all_active_todos_blocked");
|
|
151
|
+
if (todos.in_progress > 0 && liveWorking === 0 && staleClock <= staleBefore)
|
|
152
|
+
found.add("stale_in_progress_48h");
|
|
153
|
+
if (liveWorking > 0)
|
|
154
|
+
found.add("worker_working");
|
|
155
|
+
if (todos.in_progress > 0)
|
|
156
|
+
found.add("todo_in_progress");
|
|
157
|
+
if (wakeCounts.upcoming > 0)
|
|
158
|
+
found.add("wake_due_24h");
|
|
159
|
+
const HARD_STALL = [
|
|
160
|
+
"dead_lead_pane",
|
|
161
|
+
"missing_root_with_work",
|
|
162
|
+
"worker_needs_input",
|
|
163
|
+
"wake_overdue_5m",
|
|
164
|
+
];
|
|
165
|
+
const TODO_GRAPH = ["in_progress_blocked", "all_active_todos_blocked", "stale_in_progress_48h"];
|
|
166
|
+
const freshInProgress = todos.in_progress > todos.blocked_in_progress && !found.has("stale_in_progress_48h");
|
|
167
|
+
const movingSignal = liveWorking > 0 || freshInProgress;
|
|
168
|
+
let lane;
|
|
169
|
+
if (HARD_STALL.some((r) => found.has(r)))
|
|
170
|
+
lane = "stuck";
|
|
171
|
+
else if (movingSignal)
|
|
172
|
+
lane = "moving";
|
|
173
|
+
else if (found.has("needs_human"))
|
|
174
|
+
lane = "waiting_on_you";
|
|
175
|
+
else if (TODO_GRAPH.some((r) => found.has(r)))
|
|
176
|
+
lane = "stuck";
|
|
177
|
+
else {
|
|
178
|
+
lane = "quiet";
|
|
179
|
+
found.add("quiet");
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
id: project.id,
|
|
183
|
+
name: project.name,
|
|
184
|
+
root: project.path,
|
|
185
|
+
root_exists: rootExists,
|
|
186
|
+
lane,
|
|
187
|
+
reasons: PORTFOLIO_REASONS.filter((r) => found.has(r)),
|
|
188
|
+
lead,
|
|
189
|
+
workers,
|
|
190
|
+
todos,
|
|
191
|
+
needs_human: tagged.length,
|
|
192
|
+
needs_human_items: tagged.slice(0, NEEDS_HUMAN_ITEM_CAP).map((t) => ({
|
|
193
|
+
todo_id: t.id,
|
|
194
|
+
title: t.title,
|
|
195
|
+
slug: t.slug === "" ? null : t.slug,
|
|
196
|
+
updated_at: t.updated_at,
|
|
197
|
+
})),
|
|
198
|
+
last_activity_at: lastActivityAt,
|
|
199
|
+
wakes: { pending: wakeCounts.pending, overdue: wakeCounts.overdue },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
export function collectPortfolio(now) {
|
|
203
|
+
const asOf = now ?? ONE("SELECT datetime('now') AS now").now;
|
|
204
|
+
const snapshot = liveTargets();
|
|
205
|
+
const projects = listProjects().map((p) => projectRow(p, asOf, snapshot));
|
|
206
|
+
const lanes = { waiting_on_you: 0, stuck: 0, moving: 0, quiet: 0 };
|
|
207
|
+
for (const p of projects)
|
|
208
|
+
lanes[p.lane]++;
|
|
209
|
+
return { schema_version: 1, as_of: asOf, totals: { projects: projects.length, lanes }, projects };
|
|
210
|
+
}
|