@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/codexHome.js
CHANGED
|
@@ -172,6 +172,21 @@ function localOverrideHeading(path) {
|
|
|
172
172
|
export function codexInstructionsPhrase(layers) {
|
|
173
173
|
return layers.length ? `instructions: ${layers.join(", ")}` : undefined;
|
|
174
174
|
}
|
|
175
|
+
// Empty values are dropped: an unset variable and an empty one read the same to the server.
|
|
176
|
+
function mcpEnvTable(env) {
|
|
177
|
+
const entries = Object.entries(env).filter(([k, v]) => k.startsWith("HIVE_") && v !== "");
|
|
178
|
+
return [`[mcp_servers.hive.env]`, ...entries.map(([k, v]) => `${k} = ${tomlString(v)}`), ""];
|
|
179
|
+
}
|
|
180
|
+
// Resume reuses a home written at spawn, so its env table can predate a rename or this fix itself.
|
|
181
|
+
export function refreshCodexMcpEnv(key, env) {
|
|
182
|
+
const path = join(codexHomeDir(key), "config.toml");
|
|
183
|
+
if (!existsSync(path))
|
|
184
|
+
return;
|
|
185
|
+
const table = mcpEnvTable(env).join("\n") + "\n";
|
|
186
|
+
const current = readFileSync(path, "utf8");
|
|
187
|
+
const existing = /^\[mcp_servers\.hive\.env\]\n(?:(?!\[).*\n)*/m;
|
|
188
|
+
writeFileSync(path, existing.test(current) ? current.replace(existing, table) : current + "\n" + table);
|
|
189
|
+
}
|
|
175
190
|
// Must be written before any [section] header, or TOML nests it inside whichever table precedes it -
|
|
176
191
|
// accepted silently, delivered to nobody. See tmux-and-panes.md, "the brief-delivery TOML trap".
|
|
177
192
|
function configToml(input) {
|
|
@@ -190,28 +205,27 @@ function configToml(input) {
|
|
|
190
205
|
`trust_level = "trusted"`,
|
|
191
206
|
"",
|
|
192
207
|
`[mcp_servers.hive]`,
|
|
208
|
+
`default_tools_approval_mode = "approve"`,
|
|
193
209
|
`command = ${tomlString(input.nodeBin)}`,
|
|
194
210
|
`args = [${tomlString(input.indexJs)}]`,
|
|
195
211
|
"",
|
|
196
|
-
|
|
197
|
-
`HIVE_AGENT_ID = ${tomlString(input.actorId)}`,
|
|
198
|
-
"",
|
|
212
|
+
...mcpEnvTable(input.serverEnv),
|
|
199
213
|
// Only accepted under [tui]; a top-level status_line is an unknown field to --strict-config.
|
|
200
214
|
`[tui]`,
|
|
201
215
|
`status_line = ${tomlStringArray(input.statusLine)}`,
|
|
202
216
|
].join("\n") + "\n");
|
|
203
217
|
}
|
|
204
218
|
// Pure on purpose: agent_resume reuses it against an EXISTING home, so it must not write.
|
|
205
|
-
export function codexLaunchArgs(cwd) {
|
|
219
|
+
export function codexLaunchArgs(cwd, readOnly = false) {
|
|
206
220
|
const root = gitPrimaryRoot(cwd);
|
|
207
221
|
const commonDir = root ? join(root, ".git") : null;
|
|
208
222
|
return [
|
|
209
223
|
// Declining review of hive's OWN generated hooks.json, not a stranger's - see the reference.
|
|
210
224
|
"--dangerously-bypass-hook-trust",
|
|
211
225
|
// NOT parity with claude's posture, and --add-dir is NOT containment - see the reference.
|
|
212
|
-
"--dangerously-bypass-approvals-and-sandbox",
|
|
226
|
+
...(readOnly ? ["--sandbox", "read-only", "-c", 'approval_policy="never"'] : ["--dangerously-bypass-approvals-and-sandbox"]),
|
|
213
227
|
// Inert alongside the flag above, kept because its value seeds [projects.<root>] trust.
|
|
214
|
-
...(commonDir ? ["--add-dir", commonDir] : []),
|
|
228
|
+
...(commonDir && !readOnly ? ["--add-dir", commonDir] : []),
|
|
215
229
|
];
|
|
216
230
|
}
|
|
217
231
|
export function ensureCodexHooksFile(key, options) {
|
|
@@ -287,11 +301,12 @@ export function ensureCodexHome(input) {
|
|
|
287
301
|
projectRoot,
|
|
288
302
|
nodeBin: process.execPath,
|
|
289
303
|
indexJs,
|
|
290
|
-
|
|
304
|
+
serverEnv: { ...input.serverEnv, HIVE_AGENT_ID: input.actorId },
|
|
291
305
|
statusLine,
|
|
306
|
+
readOnly: input.readOnly,
|
|
292
307
|
modelContextWindow,
|
|
293
308
|
modelAutoCompactTokenLimit,
|
|
294
309
|
}));
|
|
295
310
|
const instructionLayers = [...(hasGlobal ? ["global"] : []), ...(localOverride ? ["local"] : [])];
|
|
296
|
-
return { extraArgs: codexLaunchArgs(input.cwd), hooksWired, instructionLayers };
|
|
311
|
+
return { extraArgs: codexLaunchArgs(input.cwd, input.readOnly), hooksWired, instructionLayers };
|
|
297
312
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,72 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { storeDir } from "./dataDir.js";
|
|
4
|
-
export const ATTACH_MODES = ["auto", "raw", "control"];
|
|
5
|
-
export function isAttachMode(value) {
|
|
6
|
-
return ATTACH_MODES.includes(value);
|
|
7
|
-
}
|
|
8
|
-
export const AUTO_ATTACH_MODES = ["auto", "on", "off"];
|
|
9
|
-
export function isAutoAttach(value) {
|
|
10
|
-
return AUTO_ATTACH_MODES.includes(value);
|
|
11
|
-
}
|
|
12
|
-
function configPath() {
|
|
13
|
-
return join(storeDir(), "config.json");
|
|
14
|
-
}
|
|
15
|
-
function readConfig() {
|
|
16
|
-
const path = configPath();
|
|
17
|
-
let raw;
|
|
18
|
-
try {
|
|
19
|
-
raw = readFileSync(path, "utf8");
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return {};
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const parsed = JSON.parse(raw);
|
|
26
|
-
return parsed !== null && typeof parsed === "object" ? parsed : {};
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function envAttachMode() {
|
|
33
|
-
const value = process.env.HIVE_ATTACH_MODE;
|
|
34
|
-
return isAttachMode(value) ? value : null;
|
|
35
|
-
}
|
|
36
|
-
function envAutoAttach() {
|
|
37
|
-
const value = process.env.HIVE_AUTO_ATTACH;
|
|
38
|
-
if (value === "0")
|
|
39
|
-
return "off";
|
|
40
|
-
return isAutoAttach(value) ? value : null;
|
|
41
|
-
}
|
|
1
|
+
import { readGlobalConfig, resolveAttachSetting, writeGlobalConfigKey } from "./globalConfig.js";
|
|
2
|
+
export { ATTACH_MODES, AUTO_ATTACH_MODES, isAttachMode, isAutoAttach, migrateLegacyConfig, } from "./globalConfig.js";
|
|
42
3
|
export function resolvedAttachMode() {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
return { mode: fromEnv, source: "env" };
|
|
46
|
-
const value = readConfig().attach;
|
|
47
|
-
return isAttachMode(value) ? { mode: value, source: "config" } : { mode: "auto", source: "detection" };
|
|
4
|
+
const global = readGlobalConfig();
|
|
5
|
+
return resolveAttachSetting("attach", global.root, global.legacy, global.warnings);
|
|
48
6
|
}
|
|
49
7
|
export function attachMode() {
|
|
50
8
|
return resolvedAttachMode().mode;
|
|
51
9
|
}
|
|
52
10
|
export function resolvedAutoAttach() {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
return { value: fromEnv, source: "env" };
|
|
56
|
-
const value = readConfig().autoAttach;
|
|
57
|
-
return isAutoAttach(value)
|
|
58
|
-
? { value, source: "config" }
|
|
59
|
-
: { value: "auto", source: "detection" };
|
|
11
|
+
const global = readGlobalConfig();
|
|
12
|
+
return resolveAttachSetting("autoAttach", global.root, global.legacy, global.warnings);
|
|
60
13
|
}
|
|
61
14
|
export function setAttachMode(mode) {
|
|
62
|
-
|
|
63
|
-
config.attach = mode;
|
|
64
|
-
mkdirSync(storeDir(), { recursive: true });
|
|
65
|
-
writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}\n`);
|
|
15
|
+
writeGlobalConfigKey("attach", mode);
|
|
66
16
|
}
|
|
67
17
|
export function setAutoAttach(value) {
|
|
68
|
-
|
|
69
|
-
config.autoAttach = value;
|
|
70
|
-
mkdirSync(storeDir(), { recursive: true });
|
|
71
|
-
writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}\n`);
|
|
18
|
+
writeGlobalConfigKey("autoAttach", value);
|
|
72
19
|
}
|
package/dist/context.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
3
3
|
import { userInfo } from "node:os";
|
|
4
|
-
import { basename, dirname, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
|
+
import { storeDir } from "./dataDir.js";
|
|
5
6
|
import { db } from "./db.js";
|
|
7
|
+
import { LEAD_KIND } from "./spawn.js";
|
|
6
8
|
let selectedId = null;
|
|
7
9
|
let cachedActorId = null;
|
|
8
10
|
let lastTouchMs = 0;
|
|
@@ -254,3 +256,190 @@ export function trySelectedProject() {
|
|
|
254
256
|
return null;
|
|
255
257
|
}
|
|
256
258
|
}
|
|
259
|
+
export const QUEEN_PROFILE = "queen";
|
|
260
|
+
export function queenHomeDir() {
|
|
261
|
+
return join(storeDir(), "queen");
|
|
262
|
+
}
|
|
263
|
+
function queenHomeProject() {
|
|
264
|
+
const home = queenHomeDir();
|
|
265
|
+
try {
|
|
266
|
+
if (lstatSync(home).isSymbolicLink())
|
|
267
|
+
return null;
|
|
268
|
+
return getProjectByPath(realpathSync(home)) ?? null;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function queenProjectId() {
|
|
275
|
+
if (projectLock)
|
|
276
|
+
return null;
|
|
277
|
+
const actorId = process.env.HIVE_AGENT_ID;
|
|
278
|
+
if (!actorId)
|
|
279
|
+
return null;
|
|
280
|
+
const home = queenHomeProject();
|
|
281
|
+
if (!home)
|
|
282
|
+
return null;
|
|
283
|
+
const row = db
|
|
284
|
+
.prepare("SELECT 1 FROM agents WHERE actor_id = ? AND project_id = ? AND kind = ? AND status = 'running'")
|
|
285
|
+
.get(actorId, home.id, LEAD_KIND);
|
|
286
|
+
return row ? home.id : null;
|
|
287
|
+
}
|
|
288
|
+
export function isQueenLead() {
|
|
289
|
+
return queenProjectId() !== null;
|
|
290
|
+
}
|
|
291
|
+
export function captureQueenWriteIdentity(targetProjectId) {
|
|
292
|
+
const home = queenProjectId();
|
|
293
|
+
if (home === null || home === targetProjectId)
|
|
294
|
+
return null;
|
|
295
|
+
return { actor_id: process.env.HIVE_AGENT_ID, home_project_id: home };
|
|
296
|
+
}
|
|
297
|
+
export const QUEEN_REACH = {
|
|
298
|
+
whoami: "read",
|
|
299
|
+
help: "read",
|
|
300
|
+
project_list: "read",
|
|
301
|
+
pad_list: "read",
|
|
302
|
+
pad_read: "read",
|
|
303
|
+
todo_list: "read",
|
|
304
|
+
todo_get: "read",
|
|
305
|
+
kv_get: "read",
|
|
306
|
+
kv_list: "read",
|
|
307
|
+
wake_get: "read",
|
|
308
|
+
wake_list: "read",
|
|
309
|
+
agent_list: "read",
|
|
310
|
+
agent_status: "read",
|
|
311
|
+
agent_output: "read",
|
|
312
|
+
agent_message_get: "read",
|
|
313
|
+
queen_audit_list: "read",
|
|
314
|
+
todo_create: "todo",
|
|
315
|
+
todo_comment: "todo",
|
|
316
|
+
wake_set: "lead_wake",
|
|
317
|
+
wake_when_idle: "lead_wake",
|
|
318
|
+
wake_update: "owned_lead_wake",
|
|
319
|
+
wake_cancel: "owned_lead_wake",
|
|
320
|
+
agent_send: "lead_text",
|
|
321
|
+
pad_write: "home",
|
|
322
|
+
pad_append: "home",
|
|
323
|
+
pad_edit: "home",
|
|
324
|
+
pad_archive: "home",
|
|
325
|
+
pad_delete: "home",
|
|
326
|
+
todo_update: "home",
|
|
327
|
+
todo_archive: "home",
|
|
328
|
+
todo_complete: "home",
|
|
329
|
+
todo_block: "home",
|
|
330
|
+
todo_unblock: "home",
|
|
331
|
+
kv_set: "home",
|
|
332
|
+
kv_delete: "home",
|
|
333
|
+
lease_acquire: "home",
|
|
334
|
+
lease_release: "home",
|
|
335
|
+
agent_spawn: "home",
|
|
336
|
+
agent_resume: "home",
|
|
337
|
+
agent_park: "home",
|
|
338
|
+
agent_rename: "home",
|
|
339
|
+
agent_close: "home",
|
|
340
|
+
project_select: "select",
|
|
341
|
+
project_add: "global",
|
|
342
|
+
project_prune: "global",
|
|
343
|
+
actor_prune: "global",
|
|
344
|
+
"hive lead": "spin_up",
|
|
345
|
+
"hive queen-audit": "read",
|
|
346
|
+
"hive init": "home",
|
|
347
|
+
"hive attach": "home",
|
|
348
|
+
"hive next": "home",
|
|
349
|
+
"hive start": "home",
|
|
350
|
+
"hive stop": "home",
|
|
351
|
+
"hive show": "home",
|
|
352
|
+
"hive hide": "home",
|
|
353
|
+
"hive pad --save": "home",
|
|
354
|
+
};
|
|
355
|
+
export const QUEEN_REFUSAL = "QUEEN_CROSS_PROJECT_WRITE_REFUSED";
|
|
356
|
+
function runningLead(projectId) {
|
|
357
|
+
return db
|
|
358
|
+
.prepare("SELECT id, actor_id, name FROM agents WHERE project_id = ? AND kind = ? AND status = 'running' ORDER BY id LIMIT 1")
|
|
359
|
+
.get(projectId, LEAD_KIND);
|
|
360
|
+
}
|
|
361
|
+
function namesLead(ref, lead) {
|
|
362
|
+
return typeof ref === "number" ? ref === lead.id : ref === lead.name;
|
|
363
|
+
}
|
|
364
|
+
function foreignWriteProblem(reach, targetProjectId, args) {
|
|
365
|
+
switch (reach) {
|
|
366
|
+
case "spin_up":
|
|
367
|
+
case "todo":
|
|
368
|
+
return null;
|
|
369
|
+
case "lead_wake": {
|
|
370
|
+
if (args.scope !== undefined)
|
|
371
|
+
return "a standing watch in another project belongs to its lead";
|
|
372
|
+
const lead = runningLead(targetProjectId);
|
|
373
|
+
if (!lead)
|
|
374
|
+
return "that project has no running lead to address";
|
|
375
|
+
if (!namesLead(args.deliver_to, lead)) {
|
|
376
|
+
return `deliver_to must name that project's running lead ("${lead.name}" or agent id ${lead.id})`;
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
case "owned_lead_wake": {
|
|
381
|
+
const lead = runningLead(targetProjectId);
|
|
382
|
+
if (!lead)
|
|
383
|
+
return "that project has no running lead, so no wake of yours there is still addressed to one";
|
|
384
|
+
const wake = db
|
|
385
|
+
.prepare("SELECT owner, deliver_actor FROM wakes WHERE id = ? AND project_id = ?")
|
|
386
|
+
.get(args.wake_id, targetProjectId);
|
|
387
|
+
if (!wake || wake.owner !== process.env.HIVE_AGENT_ID || wake.deliver_actor !== lead.actor_id) {
|
|
388
|
+
return "only a wake you set there, addressed to that project's current running lead, can be changed";
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
case "lead_text": {
|
|
393
|
+
if (args.keys !== undefined)
|
|
394
|
+
return "keys can interrupt a lead mid-turn; send text instead";
|
|
395
|
+
const lead = runningLead(targetProjectId);
|
|
396
|
+
if (!lead)
|
|
397
|
+
return "that project has no running lead to address";
|
|
398
|
+
if (!namesLead(args.agent_id ?? args.name, lead)) {
|
|
399
|
+
return `the recipient must be that project's running lead ("${lead.name}" or agent id ${lead.id})`;
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
case "home":
|
|
404
|
+
return "that belongs to the project's own lead";
|
|
405
|
+
case "select":
|
|
406
|
+
return "a selection would redirect every later call that omits project_id; pass project_id on each call instead";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// targetProjectId null means the operation has no single project (a store-wide sweep, an unregistered path).
|
|
410
|
+
export function assertQueenCrossProjectWrite(operation, targetProjectId, args) {
|
|
411
|
+
const home = queenProjectId();
|
|
412
|
+
if (home === null)
|
|
413
|
+
return;
|
|
414
|
+
const reach = QUEEN_REACH[operation];
|
|
415
|
+
if (reach === "read")
|
|
416
|
+
return;
|
|
417
|
+
const problem = reach === undefined
|
|
418
|
+
? "hive has not classified this operation for the queen, so it refuses by default"
|
|
419
|
+
: reach === "global"
|
|
420
|
+
? "it changes the whole store, not one project"
|
|
421
|
+
: targetProjectId === home
|
|
422
|
+
? null
|
|
423
|
+
: targetProjectId === null
|
|
424
|
+
? "it names no registered project"
|
|
425
|
+
: foreignWriteProblem(reach, targetProjectId, args);
|
|
426
|
+
if (problem)
|
|
427
|
+
throw queenRefusal(operation, targetProjectId, problem);
|
|
428
|
+
}
|
|
429
|
+
export function assertQueenToolCall(tool, args) {
|
|
430
|
+
if (!isQueenLead())
|
|
431
|
+
return;
|
|
432
|
+
const reach = QUEEN_REACH[tool];
|
|
433
|
+
if (reach === "read")
|
|
434
|
+
return;
|
|
435
|
+
const override = typeof args.project_id === "number" ? args.project_id : undefined;
|
|
436
|
+
const target = reach === undefined || reach === "global" ? null : effectiveProjectId(override);
|
|
437
|
+
assertQueenCrossProjectWrite(tool, target, args);
|
|
438
|
+
}
|
|
439
|
+
function queenRefusal(operation, targetProjectId, problem) {
|
|
440
|
+
const target = targetProjectId === null ? null : getProject(targetProjectId);
|
|
441
|
+
const where = target ? `in project ${target.id} ("${target.name}")` : "outside the queen's own project";
|
|
442
|
+
return new Error(`${QUEEN_REFUSAL}: the queen cannot run ${operation} ${where}: ${problem}. Into another project the ` +
|
|
443
|
+
"queen may only todo_create, todo_comment, send text or wakes to its running lead, and start its lead " +
|
|
444
|
+
"with `hive lead <path>`. Everything else there is its lead's to do.");
|
|
445
|
+
}
|
package/dist/db.js
CHANGED
|
@@ -368,6 +368,46 @@ ALTER TABLE wake_block_notices RENAME COLUMN timer_id TO wake_id;
|
|
|
368
368
|
CREATE INDEX idx_wakes_active ON wakes(project_id, kind)
|
|
369
369
|
WHERE cancelled_at IS NULL;
|
|
370
370
|
CREATE INDEX idx_wakes_parent ON wakes(parent_wake_id) WHERE parent_wake_id IS NOT NULL;
|
|
371
|
+
`,
|
|
372
|
+
`
|
|
373
|
+
ALTER TABLE agents ADD COLUMN model TEXT;
|
|
374
|
+
ALTER TABLE agents ADD COLUMN extra_args TEXT;
|
|
375
|
+
`,
|
|
376
|
+
`
|
|
377
|
+
CREATE TABLE lead_turn_state (
|
|
378
|
+
agent_id INTEGER PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
|
|
379
|
+
pane_pid TEXT NOT NULL DEFAULT '',
|
|
380
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
381
|
+
state TEXT NOT NULL DEFAULT 'unknown' CHECK (state IN ('unknown', 'working', 'idle')),
|
|
382
|
+
idle_seq INTEGER NOT NULL DEFAULT 0,
|
|
383
|
+
last_event TEXT NOT NULL DEFAULT '',
|
|
384
|
+
changed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
CREATE TABLE lead_idle_subscriptions (
|
|
388
|
+
wake_id INTEGER PRIMARY KEY REFERENCES wakes(id) ON DELETE CASCADE,
|
|
389
|
+
target_project_id INTEGER NOT NULL,
|
|
390
|
+
agent_id INTEGER NOT NULL,
|
|
391
|
+
pane_pid TEXT NOT NULL,
|
|
392
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
393
|
+
baseline_idle_seq INTEGER NOT NULL DEFAULT 0,
|
|
394
|
+
terminal_reason TEXT
|
|
395
|
+
);
|
|
396
|
+
`,
|
|
397
|
+
`
|
|
398
|
+
CREATE TABLE queen_audit (
|
|
399
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
400
|
+
actor_id TEXT NOT NULL,
|
|
401
|
+
home_project_id INTEGER NOT NULL,
|
|
402
|
+
target_project_id INTEGER NOT NULL,
|
|
403
|
+
operation TEXT NOT NULL,
|
|
404
|
+
resource_type TEXT NOT NULL,
|
|
405
|
+
resource_id INTEGER NOT NULL CHECK(resource_id > 0),
|
|
406
|
+
summary TEXT NOT NULL CHECK(length(summary) <= 160),
|
|
407
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')),
|
|
408
|
+
CHECK(home_project_id != target_project_id)
|
|
409
|
+
);
|
|
410
|
+
CREATE INDEX idx_queen_audit_target ON queen_audit(target_project_id, id);
|
|
371
411
|
`,
|
|
372
412
|
];
|
|
373
413
|
export function storeSchemaAhead(database) {
|
|
@@ -385,48 +425,68 @@ function readAppliedVersions() {
|
|
|
385
425
|
return new Set(db.prepare("SELECT version FROM migrations").all().map((r) => r.version));
|
|
386
426
|
}
|
|
387
427
|
const MIGRATE_LOCK_MAX_ATTEMPTS = 24;
|
|
388
|
-
function retryOnBusy(fn, label) {
|
|
428
|
+
function retryOnBusy(fn, label, pending) {
|
|
389
429
|
for (let attempt = 1;; attempt++) {
|
|
390
430
|
try {
|
|
391
431
|
return fn();
|
|
392
432
|
}
|
|
393
433
|
catch (e) {
|
|
394
434
|
const busy = e instanceof Error && (e.code === "SQLITE_BUSY" || /database is locked/.test(e.message));
|
|
395
|
-
if (
|
|
435
|
+
if (busy && attempt < MIGRATE_LOCK_MAX_ATTEMPTS)
|
|
436
|
+
continue;
|
|
437
|
+
if (busy) {
|
|
396
438
|
console.error(`hive: ${label} did not complete after ${attempt} attempt(s): the store's write lock never freed ` +
|
|
397
439
|
"up. Another hive process is likely still applying a migration (or is stuck); check for a " +
|
|
398
440
|
`wedged hive process and try again. Underlying error: ${errorMessage(e)}`);
|
|
399
|
-
process.exit(1);
|
|
400
441
|
}
|
|
442
|
+
else {
|
|
443
|
+
console.error(`hive: ${label} failed: ${errorMessage(e)}${pending ? ` (${pending})` : ""}`);
|
|
444
|
+
}
|
|
445
|
+
process.exit(1);
|
|
401
446
|
}
|
|
402
447
|
}
|
|
403
448
|
}
|
|
449
|
+
function bookkeepingMissing() {
|
|
450
|
+
const names = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('migrations', 'backup_meta')").all().length;
|
|
451
|
+
return names < 2 || db.prepare("SELECT 1 FROM backup_meta WHERE id = 1").get() === undefined;
|
|
452
|
+
}
|
|
404
453
|
export function migrate() {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
454
|
+
if (bookkeepingMissing()) {
|
|
455
|
+
retryOnBusy(() => {
|
|
456
|
+
db.exec(`CREATE TABLE IF NOT EXISTS migrations (
|
|
457
|
+
version INTEGER PRIMARY KEY,
|
|
458
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
459
|
+
)`);
|
|
460
|
+
db.exec(`CREATE TABLE IF NOT EXISTS backup_meta (
|
|
461
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
462
|
+
last_attempt_at TEXT,
|
|
463
|
+
last_success_at TEXT,
|
|
464
|
+
last_error TEXT,
|
|
465
|
+
last_error_at TEXT
|
|
466
|
+
)`);
|
|
467
|
+
db.prepare("INSERT OR IGNORE INTO backup_meta (id) VALUES (1)").run();
|
|
468
|
+
}, "preparing the store's bookkeeping tables", "bookkeeping missing");
|
|
469
|
+
}
|
|
419
470
|
const before = readAppliedVersions();
|
|
420
|
-
|
|
471
|
+
const pendingCount = MIGRATIONS.length - before.size;
|
|
472
|
+
maybeBackupBeforeMigrations(db, dataDir, pendingCount);
|
|
421
473
|
const applyPending = db.transaction(() => {
|
|
422
474
|
const applied = readAppliedVersions();
|
|
423
475
|
MIGRATIONS.forEach((sql, i) => {
|
|
424
476
|
const version = i + 1;
|
|
425
477
|
if (applied.has(version))
|
|
426
478
|
return;
|
|
427
|
-
|
|
479
|
+
try {
|
|
480
|
+
db.exec(sql);
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
const wrapped = new Error(`migration ${version}: ${errorMessage(e)}`);
|
|
484
|
+
if (e instanceof Error)
|
|
485
|
+
wrapped.code = e.code;
|
|
486
|
+
throw wrapped;
|
|
487
|
+
}
|
|
428
488
|
db.prepare("INSERT INTO migrations (version) VALUES (?)").run(version);
|
|
429
489
|
});
|
|
430
490
|
});
|
|
431
|
-
retryOnBusy(() => applyPending.immediate(), "applying pending migrations");
|
|
491
|
+
retryOnBusy(() => applyPending.immediate(), "applying pending migrations", pendingCount > 0 ? `${pendingCount} migration${pendingCount === 1 ? "" : "s"} pending` : undefined);
|
|
432
492
|
}
|