@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/projectYml.js
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
return {
|
|
45
|
-
|
|
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
|
-
|
|
77
|
+
text = readFileSync(path, "utf8");
|
|
48
78
|
}
|
|
49
|
-
catch (
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
warnings.push(
|
|
111
|
+
if (!isMapping(item)) {
|
|
112
|
+
warnings.push(`${path}: Process "${name}" needs a command string; skipped.`);
|
|
113
|
+
continue;
|
|
82
114
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
if (typeof
|
|
172
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
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,
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|