@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.
Files changed (48) hide show
  1. package/README.md +6 -1
  2. package/claude-plugin/README.md +4 -1
  3. package/claude-plugin/kickoff.mjs +2 -1
  4. package/claude-plugin/skills/profile/SKILL.md +78 -0
  5. package/claude-plugin/skills/profile/references/interview.md +139 -0
  6. package/claude-plugin/skills/profile/references/recipes/index.md +12 -0
  7. package/claude-plugin/skills/profile/references/recipes/review-decision.md +41 -0
  8. package/claude-plugin/skills/profile/references/recipes/session-continuity.md +37 -0
  9. package/claude-plugin/skills/profile/references/recipes/shared-test-resource.md +45 -0
  10. package/claude-plugin/skills/profile/references/recipes/verification.md +50 -0
  11. package/claude-plugin/skills/profile/references/recipes/worker-context-checkpoint.md +51 -0
  12. package/dist/brief.js +1 -1
  13. package/dist/build-info.json +1 -1
  14. package/dist/cli.js +436 -112
  15. package/dist/codexHome.js +23 -8
  16. package/dist/config.js +8 -61
  17. package/dist/context.js +191 -2
  18. package/dist/db.js +80 -20
  19. package/dist/firstMessage.js +5 -0
  20. package/dist/globalConfig.js +247 -0
  21. package/dist/harnesses.js +14 -4
  22. package/dist/help.js +29 -11
  23. package/dist/hook.js +9 -0
  24. package/dist/hooks.js +2 -1
  25. package/dist/index.js +11 -5
  26. package/dist/kickoff.js +42 -29
  27. package/dist/leadState.js +63 -0
  28. package/dist/leadWatch.js +73 -0
  29. package/dist/portfolio.js +210 -0
  30. package/dist/projectYml.js +242 -178
  31. package/dist/queenAudit.js +118 -0
  32. package/dist/queenDashboard.js +573 -0
  33. package/dist/scheduler.js +190 -20
  34. package/dist/spawn.js +17 -4
  35. package/dist/strictInput.js +24 -7
  36. package/dist/tmux.js +65 -0
  37. package/dist/tools/agents.js +153 -15
  38. package/dist/tools/kv.js +24 -0
  39. package/dist/tools/leases.js +14 -2
  40. package/dist/tools/meta.js +65 -3
  41. package/dist/tools/pads.js +42 -0
  42. package/dist/tools/queenAudit.js +13 -0
  43. package/dist/tools/todos.js +72 -13
  44. package/dist/tools/wakes.js +233 -65
  45. package/package.json +4 -4
  46. package/profiles/orchestration/worker.md +1 -1
  47. package/profiles/queen/posture.md +31 -0
  48. package/profiles/queen/runbook.md +26 -0
@@ -1,11 +1,30 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, readFileSync, realpathSync } from "node:fs";
3
- import { join, resolve, sep } from "node:path";
2
+ import { readFileSync, realpathSync } from "node:fs";
3
+ import { resolve, sep } from "node:path";
4
4
  import { parse } from "yaml";
5
5
  import { harnessNames } from "./harnesses.js";
6
6
  import { isValidProfileName } from "./profiles.js";
7
7
  import { errorMessage } from "./result.js";
8
8
  import { isWindowLayout, WINDOW_LAYOUTS } from "./tmux.js";
9
+ import { globalConfigPath, isEmptyYamlDocument, readGlobalConfig, resolveAttachSetting, } from "./globalConfig.js";
10
+ export const BUILT_IN_PROJECT_YML = Object.freeze({
11
+ lead: null,
12
+ placement: null,
13
+ layout: null,
14
+ profile: null,
15
+ agents: null,
16
+ lead_branches: null,
17
+ first_message: null,
18
+ context_checkpoint_percent: null,
19
+ lead_turn_budget: null,
20
+ dashboard: false,
21
+ vars: Object.freeze({}),
22
+ processes: Object.freeze({}),
23
+ });
24
+ export const PROJECT_YML_KEYS = Object.freeze(Object.keys(BUILT_IN_PROJECT_YML));
25
+ export function createBuiltInProjectYml() {
26
+ return structuredClone(BUILT_IN_PROJECT_YML);
27
+ }
9
28
  export const DEFAULT_LEAD_BRANCHES = ["main", "master"];
10
29
  export const NO_PROFILE = "none";
11
30
  export function activeProfile(config) {
@@ -37,224 +56,269 @@ export function mergedProjectVars(config) {
37
56
  delete safe[key];
38
57
  return { ...safe, ...agentVars(config) };
39
58
  }
40
- export function loadProjectYml(projectPath) {
41
- const warnings = [];
42
- const file = join(projectPath, "hive.yml");
43
- if (!existsSync(file))
44
- return { config: null, warnings };
45
- let raw;
59
+ // Absent sends nothing; "" (or whitespace only) also sends nothing, and a project one beats a global message.
60
+ export function resolveFirstMessage(config, sources = {}) {
61
+ const value = config?.first_message;
62
+ if (value == null)
63
+ return { message: "", source: "unset" };
64
+ if (value.trim() === "")
65
+ return { message: "", source: "empty" };
66
+ return { message: value, source: sources.first_message === "global" ? "global" : "project" };
67
+ }
68
+ function isMapping(value) {
69
+ if (value === null || typeof value !== "object" || Array.isArray(value))
70
+ return false;
71
+ const prototype = Object.getPrototypeOf(value);
72
+ return prototype === Object.prototype || prototype === null;
73
+ }
74
+ function parseFile(path, warnings) {
75
+ let text;
46
76
  try {
47
- raw = parse(readFileSync(file, "utf8"));
77
+ text = readFileSync(path, "utf8");
48
78
  }
49
- catch (e) {
50
- warnings.push(`hive.yml is not valid YAML: ${errorMessage(e)}`);
51
- return { config: null, warnings };
52
- }
53
- if (raw == null || typeof raw !== "object") {
54
- warnings.push("hive.yml must be a YAML mapping.");
55
- return { config: null, warnings };
56
- }
57
- const root = raw;
58
- let context_checkpoint_percent = null;
59
- if (root.context_checkpoint_percent != null) {
60
- const value = root.context_checkpoint_percent;
61
- if (typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 100) {
62
- context_checkpoint_percent = value;
79
+ catch (error) {
80
+ if (error.code !== "ENOENT") {
81
+ warnings.push(`${path}: could not read hive.yml: ${errorMessage(error)}`);
63
82
  }
64
- else {
65
- warnings.push("context_checkpoint_percent must be an integer from 1 through 100; ignoring it.");
83
+ return null;
84
+ }
85
+ try {
86
+ const raw = parse(text);
87
+ if (raw == null && isEmptyYamlDocument(text))
88
+ return null;
89
+ if (!isMapping(raw)) {
90
+ warnings.push(`${path}: hive.yml must be a YAML mapping.`);
91
+ return null;
66
92
  }
93
+ return raw;
67
94
  }
68
- let lead_turn_budget = null;
69
- if (root.lead_turn_budget != null) {
70
- const value = root.lead_turn_budget;
71
- const budget = typeof value === "object" && value !== null && !Array.isArray(value)
72
- ? value
73
- : null;
74
- const warn = budget?.warn;
75
- const stop = budget?.stop;
76
- if (typeof warn === "number" && Number.isInteger(warn) && warn > 0 &&
77
- typeof stop === "number" && Number.isInteger(stop) && stop > warn) {
78
- lead_turn_budget = { warn, stop };
95
+ catch (error) {
96
+ warnings.push(`${path}: hive.yml is not valid YAML: ${errorMessage(error)}`);
97
+ return null;
98
+ }
99
+ }
100
+ function parseProcesses(value, path, warnings) {
101
+ if (!isMapping(value)) {
102
+ warnings.push(`${path}: hive.yml \`processes\` must be a mapping of name to command.`);
103
+ return null;
104
+ }
105
+ const processes = {};
106
+ for (const [name, item] of Object.entries(value)) {
107
+ if (typeof item === "string" && item.trim() !== "") {
108
+ processes[name] = { command: item.trim(), dir: null, auto_start: true, visible: true, env: {} };
109
+ continue;
79
110
  }
80
- else {
81
- warnings.push("lead_turn_budget must contain positive integer warn and stop values, with stop greater than warn; ignoring it.");
111
+ if (!isMapping(item)) {
112
+ warnings.push(`${path}: Process "${name}" needs a command string; skipped.`);
113
+ continue;
82
114
  }
83
- }
84
- const lead = typeof root.lead === "string" && root.lead.trim() !== "" ? root.lead.trim() : null;
85
- let placement = null;
86
- if (root.placement != null) {
87
- if (root.placement === "split" || root.placement === "window") {
88
- placement = root.placement;
115
+ if (typeof item.command !== "string" || item.command.trim() === "") {
116
+ warnings.push(`${path}: Process "${name}" has no command; skipped.`);
117
+ continue;
89
118
  }
90
- else {
91
- warnings.push(`placement must be "split" or "window"; ignoring "${String(root.placement)}".`);
119
+ const dirValue = item.dir ?? item.working_dir;
120
+ let visible = true;
121
+ if (item.visible != null) {
122
+ if (typeof item.visible === "boolean")
123
+ visible = item.visible;
124
+ else
125
+ warnings.push(`${path}: Process "${name}": visible must be true or false; ignoring "${String(item.visible)}".`);
92
126
  }
127
+ const env = {};
128
+ if (isMapping(item.env)) {
129
+ for (const [key, envValue] of Object.entries(item.env))
130
+ env[key] = String(envValue);
131
+ }
132
+ processes[name] = {
133
+ command: item.command.trim(),
134
+ dir: typeof dirValue === "string" && dirValue.trim() !== "" ? dirValue.trim() : null,
135
+ auto_start: item.auto_start !== false,
136
+ visible,
137
+ env,
138
+ };
93
139
  }
94
- let layout = null;
95
- if (root.layout != null) {
96
- if (isWindowLayout(root.layout)) {
97
- layout = root.layout;
140
+ return processes;
141
+ }
142
+ function applyLayer(root, path, source, config, sources, warnings) {
143
+ const has = (key) => Object.hasOwn(root, key);
144
+ const warn = (message) => warnings.push(`${path}: ${message}`);
145
+ const assignNullable = (key, valid, message) => {
146
+ if (!has(key))
147
+ return;
148
+ const value = root[key];
149
+ if (value == null) {
150
+ config[key] = null;
151
+ sources[key] = source;
98
152
  }
99
153
  else {
100
- warnings.push(`layout must be one of ${WINDOW_LAYOUTS.join(", ")}; ignoring "${String(root.layout)}".`);
154
+ const normalized = valid(value);
155
+ if (normalized === undefined)
156
+ warn(message(value));
157
+ else {
158
+ config[key] = normalized;
159
+ sources[key] = source;
160
+ }
101
161
  }
102
- }
103
- let profile = null;
104
- if (root.profile != null) {
105
- const value = String(root.profile).trim();
106
- if (value === "") {
107
- warnings.push("profile is empty; ignoring it.");
162
+ };
163
+ assignNullable("lead", (value) => typeof value === "string" && value.trim() ? value.trim() : undefined, (value) => `lead must be a non-empty command string; ignoring "${String(value)}".`);
164
+ assignNullable("placement", (value) => value === "split" || value === "window" ? value : undefined, (value) => `placement must be "split" or "window"; ignoring "${String(value)}".`);
165
+ assignNullable("layout", (value) => isWindowLayout(value) ? value : undefined, (value) => `layout must be one of ${WINDOW_LAYOUTS.join(", ")}; ignoring "${String(value)}".`);
166
+ assignNullable("profile", (value) => {
167
+ const name = String(value).trim();
168
+ return name !== "" && (name === NO_PROFILE || isValidProfileName(name)) ? name : undefined;
169
+ }, (value) => typeof value === "string" && value.trim() === ""
170
+ ? "profile is empty; ignoring it."
171
+ : `profile "${String(value).trim()}" is not a valid profile name (letters, digits, dot, dash, underscore); ignoring it.`);
172
+ assignNullable("context_checkpoint_percent", (value) => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 100 ? value : undefined, () => "context_checkpoint_percent must be an integer from 1 through 100; ignoring it.");
173
+ if (has("lead_turn_budget")) {
174
+ const value = root.lead_turn_budget;
175
+ if (value == null) {
176
+ config.lead_turn_budget = null;
177
+ sources.lead_turn_budget = source;
108
178
  }
109
- else if (value !== NO_PROFILE && !isValidProfileName(value)) {
110
- warnings.push(`profile "${value}" is not a valid profile name (letters, digits, dot, dash, underscore); ignoring it.`);
179
+ else if (isMapping(value) && typeof value.warn === "number" && Number.isInteger(value.warn) && value.warn > 0 &&
180
+ typeof value.stop === "number" && Number.isInteger(value.stop) && value.stop > value.warn) {
181
+ config.lead_turn_budget = { warn: value.warn, stop: value.stop };
182
+ sources.lead_turn_budget = source;
111
183
  }
112
184
  else {
113
- profile = value;
185
+ warn("lead_turn_budget must contain positive integer warn and stop values, with stop greater than warn; ignoring it.");
114
186
  }
115
187
  }
116
- let agents = null;
117
- if (root.agents != null) {
118
- if (!Array.isArray(root.agents)) {
119
- warnings.push("agents must be a list of harness names; ignoring it.");
188
+ if (has("dashboard")) {
189
+ const value = root.dashboard;
190
+ if (value == null) {
191
+ config.dashboard = false;
192
+ sources.dashboard = source;
120
193
  }
121
- else {
122
- const known = harnessNames();
123
- const valid = [];
124
- for (const entry of root.agents) {
125
- const value = typeof entry === "string" ? entry.trim() : "";
126
- if (value === "") {
127
- warnings.push("agents entry must be a non-empty harness name; skipped.");
128
- }
129
- else if (!known.includes(value)) {
130
- warnings.push(`agents: "${value}" is not a known harness (${known.join(", ")}); ignoring it.`);
131
- }
132
- else {
133
- valid.push(value);
134
- }
135
- }
136
- agents = valid.length > 0 ? valid : null;
194
+ else if (typeof value === "boolean") {
195
+ config.dashboard = value;
196
+ sources.dashboard = source;
137
197
  }
198
+ else
199
+ warn(`dashboard must be true or false; ignoring "${String(value)}".`);
138
200
  }
139
- const review_tags = [];
140
- if (root.review_tags != null) {
141
- if (!Array.isArray(root.review_tags)) {
142
- warnings.push("review_tags must be a list of todo tags; ignoring it.");
201
+ if (has("agents")) {
202
+ const value = root.agents;
203
+ if (value == null) {
204
+ config.agents = null;
205
+ sources.agents = source;
143
206
  }
144
- else {
145
- for (const entry of root.review_tags) {
146
- const value = typeof entry === "string" ? entry.trim() : "";
147
- if (value === "") {
148
- warnings.push("review_tags entry must be a non-empty tag; skipped.");
149
- }
150
- else if (!review_tags.includes(value)) {
151
- review_tags.push(value);
152
- }
207
+ else if (Array.isArray(value)) {
208
+ const known = harnessNames();
209
+ const valid = [];
210
+ for (const entry of value) {
211
+ const name = typeof entry === "string" ? entry.trim() : "";
212
+ if (!name)
213
+ warn("agents entry must be a non-empty harness name; skipped.");
214
+ else if (!known.includes(name))
215
+ warn(`agents: "${name}" is not a known harness (${known.join(", ")}); ignoring it.`);
216
+ else
217
+ valid.push(name);
153
218
  }
219
+ config.agents = valid.length ? valid : null;
220
+ sources.agents = source;
154
221
  }
222
+ else
223
+ warn("agents must be a list of harness names; ignoring it.");
155
224
  }
156
- let lead_branches = null;
157
- if (root.lead_branches != null) {
158
- const raw = Array.isArray(root.lead_branches) ? root.lead_branches : null;
159
- const branches = raw
160
- ?.filter((b) => typeof b === "string" && b.trim() !== "")
161
- .map((b) => b.trim());
162
- if (branches && branches.length === raw.length) {
163
- lead_branches = branches;
225
+ if (has("lead_branches")) {
226
+ const value = root.lead_branches;
227
+ if (value == null) {
228
+ config.lead_branches = null;
229
+ sources.lead_branches = source;
164
230
  }
165
- else {
166
- warnings.push("lead_branches must be a list of branch names; ignoring it.");
231
+ else if (Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim() !== "")) {
232
+ config.lead_branches = value.map((item) => item.trim());
233
+ sources.lead_branches = source;
167
234
  }
235
+ else
236
+ warn("lead_branches must be a list of branch names; ignoring it.");
168
237
  }
169
- let dashboard = false;
170
- if (root.dashboard != null) {
171
- if (typeof root.dashboard === "boolean") {
172
- dashboard = root.dashboard;
173
- }
174
- else {
175
- warnings.push(`dashboard must be true or false; ignoring "${String(root.dashboard)}".`);
238
+ if (has("first_message")) {
239
+ const value = root.first_message;
240
+ if (value == null || typeof value === "string") {
241
+ config.first_message = value ?? "";
242
+ sources.first_message = source;
176
243
  }
244
+ else
245
+ warn("first_message must be a string; ignoring it.");
177
246
  }
178
- const vars = {};
179
- if (root.vars != null) {
180
- if (typeof root.vars !== "object" || Array.isArray(root.vars)) {
181
- warnings.push("vars must be a mapping of name to value; ignoring it.");
247
+ if (has("vars")) {
248
+ const value = root.vars;
249
+ if (value == null) {
250
+ for (const key of Object.keys(config.vars))
251
+ sources[`vars.${key}`] = source;
252
+ config.vars = {};
253
+ sources.vars = source;
182
254
  }
183
- else {
184
- for (const [key, value] of Object.entries(root.vars)) {
185
- if (value == null || typeof value === "object") {
186
- warnings.push(`var "${key}" must be a scalar; skipped.`);
187
- continue;
255
+ else if (isMapping(value)) {
256
+ for (const [key, item] of Object.entries(value)) {
257
+ if (item == null) {
258
+ delete config.vars[key];
259
+ sources[`vars.${key}`] = source;
260
+ sources.vars = source;
261
+ }
262
+ else if (typeof item === "object") {
263
+ warn(`var "${key}" must be a scalar; skipped.`);
264
+ }
265
+ else {
266
+ config.vars[key] = String(item);
267
+ sources[`vars.${key}`] = source;
268
+ sources.vars = source;
188
269
  }
189
- vars[key] = String(value);
190
270
  }
191
271
  }
272
+ else
273
+ warn("vars must be a mapping of name to value; ignoring it.");
192
274
  }
193
- const processes = {};
194
- const rawProcesses = root.processes;
195
- if (rawProcesses != null) {
196
- if (typeof rawProcesses !== "object") {
197
- warnings.push("hive.yml `processes` must be a mapping of name to command.");
198
- }
199
- else {
200
- for (const [name, value] of Object.entries(rawProcesses)) {
201
- if (typeof value === "string" && value.trim() !== "") {
202
- processes[name] = { command: value.trim(), dir: null, auto_start: true, visible: true, env: {} };
203
- continue;
204
- }
205
- if (value == null || typeof value !== "object") {
206
- warnings.push(`Process "${name}" needs a command string; skipped.`);
207
- continue;
208
- }
209
- const p = value;
210
- if (typeof p.command !== "string" || p.command.trim() === "") {
211
- warnings.push(`Process "${name}" has no command; skipped.`);
212
- continue;
213
- }
214
- const dirValue = p.dir ?? p.working_dir;
215
- let visible = true;
216
- if (p.visible != null) {
217
- if (typeof p.visible === "boolean") {
218
- visible = p.visible;
219
- }
220
- else {
221
- warnings.push(`Process "${name}": visible must be true or false; ignoring "${String(p.visible)}".`);
222
- }
223
- }
224
- const env = {};
225
- if (p.env != null && typeof p.env === "object") {
226
- for (const [k, v] of Object.entries(p.env)) {
227
- env[k] = String(v);
228
- }
229
- }
230
- processes[name] = {
231
- command: p.command.trim(),
232
- dir: typeof dirValue === "string" && dirValue.trim() !== "" ? dirValue.trim() : null,
233
- auto_start: p.auto_start !== false,
234
- visible,
235
- env,
236
- };
275
+ if (has("processes")) {
276
+ if (source === "global")
277
+ warn("global hive.yml cannot define `processes`; only project commands can be trusted.");
278
+ else if (root.processes != null) {
279
+ const processes = parseProcesses(root.processes, path, warnings);
280
+ if (processes != null) {
281
+ config.processes = processes;
282
+ sources.processes = source;
237
283
  }
238
284
  }
239
285
  }
286
+ for (const key of ["attach", "autoAttach"]) {
287
+ if (source === "project" && has(key))
288
+ warn(`${key} is global-only; ignoring it in project hive.yml.`);
289
+ }
290
+ if (has("review_tags"))
291
+ warn("review_tags is no longer used by hive; remove it from hive.yml.");
292
+ }
293
+ export function resolveHiveConfig(projectPath) {
294
+ const global = readGlobalConfig();
295
+ const warnings = [...global.warnings];
296
+ const config = createBuiltInProjectYml();
297
+ const sources = Object.fromEntries(PROJECT_YML_KEYS.map((key) => [key, "built-in"]));
298
+ if (global.root)
299
+ applyLayer(global.root, globalConfigPath(), "global", config, sources, warnings);
300
+ let projectValid = false;
301
+ if (projectPath != null) {
302
+ const file = resolve(projectPath, "hive.yml");
303
+ const project = parseFile(file, warnings);
304
+ if (project) {
305
+ projectValid = true;
306
+ applyLayer(project, file, "project", config, sources, warnings);
307
+ }
308
+ }
309
+ const attachResult = resolveAttachSetting("attach", global.root, global.legacy, warnings);
310
+ const autoAttachResult = resolveAttachSetting("autoAttach", global.root, global.legacy, warnings);
240
311
  return {
241
- config: {
242
- lead,
243
- placement,
244
- layout,
245
- profile,
246
- agents,
247
- review_tags,
248
- lead_branches,
249
- context_checkpoint_percent,
250
- lead_turn_budget,
251
- dashboard,
252
- vars,
253
- processes,
254
- },
312
+ config: global.root || projectValid ? config : null,
255
313
  warnings,
314
+ sources,
315
+ attach: attachResult,
316
+ autoAttach: autoAttachResult,
256
317
  };
257
318
  }
319
+ export function loadProjectYml(projectPath) {
320
+ return resolveHiveConfig(projectPath);
321
+ }
258
322
  export function configHash(name, command, dir, env) {
259
323
  return createHash("sha256")
260
324
  .update(JSON.stringify([name, command, dir, env]))
@@ -0,0 +1,118 @@
1
+ import { captureQueenWriteIdentity, effectiveProjectId, isQueenLead } from "./context.js";
2
+ import { db } from "./db.js";
3
+ import { cutToUnitBudget, flatten } from "./slug.js";
4
+ export const QUEEN_AUDIT_SUMMARY_MAX = 160;
5
+ export const QUEEN_AUDIT_DEFAULT_LIMIT = 20;
6
+ export const QUEEN_AUDIT_MAX_LIMIT = 100;
7
+ export const QUEEN_AUDIT_RETENTION = "-30 days";
8
+ export const QUEEN_AUDIT_MAX_ROWS = 20_000;
9
+ function resource(type, value) {
10
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? { type, id: value } : null;
11
+ }
12
+ export const QUEEN_AUDIT_OPERATIONS = {
13
+ todo_create: {
14
+ mode: "database",
15
+ resource: (r) => resource("todo", r.todo_id),
16
+ summary: (a) => String(a.title),
17
+ },
18
+ todo_comment: {
19
+ mode: "database",
20
+ resource: (r) => resource("todo_comment", r.comment_id),
21
+ summary: (a) => `todo #${a.todo_id}: ${a.body}`,
22
+ },
23
+ wake_set: {
24
+ mode: "database",
25
+ resource: (r) => resource("wake", r.wake_id),
26
+ summary: (a, r) => `to ${r.deliver_to}: ${a.body}`,
27
+ },
28
+ wake_when_idle: {
29
+ mode: "database",
30
+ resource: (r) => r.status === "already_satisfied" ? null : resource("wake", r.wake_id),
31
+ summary: (a, r) => `${a.mode ?? "any"} to ${r.deliver_to}: ${a.body}`,
32
+ },
33
+ wake_update: {
34
+ mode: "database",
35
+ resource: (r) => r.updated === true ? resource("wake", r.wake_id) : null,
36
+ summary: (a) => `changed ${["body", "delay_seconds", "repeat_every_seconds"].filter((key) => a[key] !== undefined).join(", ")}${a.body === undefined ? "" : `: ${a.body}`}`,
37
+ },
38
+ wake_cancel: {
39
+ mode: "database",
40
+ resource: (r) => r.cancelled === true ? resource("wake", r.wake_id) : null,
41
+ summary: (_a, r) => `cancelled; notices ${r.cancelled_notices ?? 0}`,
42
+ },
43
+ agent_send: {
44
+ mode: "external",
45
+ resource: (r) => r.sent === true ? resource("agent", r.agent_id) : null,
46
+ summary: (a, r) => `agent #${r.agent_id} ${a.submit === false ? "pasted without submit" : "submitted"}: ${a.text}`,
47
+ },
48
+ "hive lead": {
49
+ mode: "external",
50
+ resource: (r) => r.disposition === "started" || r.disposition === "adopted" ? resource("agent", r.agent_id) : null,
51
+ summary: (a, r) => `${r.disposition} lead #${r.agent_id}${a.detach ? " detached" : ""}`,
52
+ },
53
+ };
54
+ function descriptor(operation) {
55
+ const found = Object.hasOwn(QUEEN_AUDIT_OPERATIONS, operation) ? QUEEN_AUDIT_OPERATIONS[operation] : undefined;
56
+ if (!found)
57
+ throw new Error(`[queen_audit:missing-coverage] ${operation} has no audit descriptor`);
58
+ return found;
59
+ }
60
+ function insertAudit(operation, targetProjectId, args, receipt, identity) {
61
+ const spec = descriptor(operation);
62
+ const touched = spec.resource(receipt);
63
+ if (!touched)
64
+ return;
65
+ const text = flatten(spec.summary(args, receipt));
66
+ const summary = text.length <= QUEEN_AUDIT_SUMMARY_MAX ? text : cutToUnitBudget(text, QUEEN_AUDIT_SUMMARY_MAX - 1) + "…";
67
+ db.prepare(`INSERT INTO queen_audit (actor_id, home_project_id, target_project_id, operation, resource_type, resource_id, summary)
68
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(identity.actor_id, identity.home_project_id, targetProjectId, operation, touched.type, touched.id, summary);
69
+ }
70
+ export function commitQueenStateWrite(operation, targetProjectId, args, mutate) {
71
+ const identity = captureQueenWriteIdentity(targetProjectId);
72
+ if (!identity)
73
+ return mutate();
74
+ const spec = descriptor(operation);
75
+ if (spec.mode !== "database")
76
+ throw new Error(`[queen_audit:wrong-mode] ${operation} is not a database write`);
77
+ return db.transaction(() => {
78
+ const receipt = mutate();
79
+ insertAudit(operation, targetProjectId, args, receipt, identity);
80
+ return receipt;
81
+ }).immediate();
82
+ }
83
+ export function confirmQueenWrite(operation, targetProjectId, args, receipt, identity = captureQueenWriteIdentity(targetProjectId)) {
84
+ if (!identity)
85
+ return;
86
+ try {
87
+ insertAudit(operation, targetProjectId, args, receipt, identity);
88
+ }
89
+ catch (e) {
90
+ if (operation !== "agent_send")
91
+ throw e;
92
+ throw new Error(`[queen_audit:write-completed-audit-failed] agent #${receipt.agent_id}: the text already landed. Do not resend blindly. ${e instanceof Error ? e.message : String(e)}`);
93
+ }
94
+ }
95
+ function positiveInteger(value, name) {
96
+ if (!Number.isInteger(value) || value <= 0)
97
+ throw new Error(`${name} must be a positive integer`);
98
+ }
99
+ export function recentQueenAudit(limit) {
100
+ return db.prepare("SELECT * FROM queen_audit ORDER BY id DESC LIMIT ?").all(limit);
101
+ }
102
+ export function listQueenAudit(options = {}, homeProjectId) {
103
+ const requestedLimit = options.limit ?? QUEEN_AUDIT_DEFAULT_LIMIT;
104
+ positiveInteger(requestedLimit, "limit");
105
+ if (options.project_id !== undefined)
106
+ positiveInteger(options.project_id, "project_id");
107
+ const target = options.project_id === undefined ? null : effectiveProjectId(options.project_id);
108
+ const queen = isQueenLead();
109
+ const home = queen ? null : homeProjectId ?? effectiveProjectId();
110
+ if (!queen && target !== null && target !== home)
111
+ throw new Error("Queen audit access is limited to your own project");
112
+ const projectId = target ?? home;
113
+ const limit = Math.min(requestedLimit, QUEEN_AUDIT_MAX_LIMIT);
114
+ const entries = (projectId === null
115
+ ? db.prepare("SELECT * FROM queen_audit ORDER BY id DESC LIMIT ?").all(limit)
116
+ : db.prepare("SELECT * FROM queen_audit WHERE target_project_id = ? ORDER BY id DESC LIMIT ?").all(projectId, limit));
117
+ return { entries, limit, project_id: projectId };
118
+ }