@cmgmyr/hive 1.2.1 → 1.3.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.
@@ -1,11 +1,29 @@
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
+ context_checkpoint_percent: null,
18
+ lead_turn_budget: null,
19
+ dashboard: false,
20
+ vars: Object.freeze({}),
21
+ processes: Object.freeze({}),
22
+ });
23
+ export const PROJECT_YML_KEYS = Object.freeze(Object.keys(BUILT_IN_PROJECT_YML));
24
+ export function createBuiltInProjectYml() {
25
+ return structuredClone(BUILT_IN_PROJECT_YML);
26
+ }
9
27
  export const DEFAULT_LEAD_BRANCHES = ["main", "master"];
10
28
  export const NO_PROFILE = "none";
11
29
  export function activeProfile(config) {
@@ -37,224 +55,251 @@ export function mergedProjectVars(config) {
37
55
  delete safe[key];
38
56
  return { ...safe, ...agentVars(config) };
39
57
  }
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;
58
+ function isMapping(value) {
59
+ if (value === null || typeof value !== "object" || Array.isArray(value))
60
+ return false;
61
+ const prototype = Object.getPrototypeOf(value);
62
+ return prototype === Object.prototype || prototype === null;
63
+ }
64
+ function parseFile(path, warnings) {
65
+ let text;
46
66
  try {
47
- raw = parse(readFileSync(file, "utf8"));
48
- }
49
- catch (e) {
50
- warnings.push(`hive.yml is not valid YAML: ${errorMessage(e)}`);
51
- return { config: null, warnings };
67
+ text = readFileSync(path, "utf8");
52
68
  }
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;
63
- }
64
- else {
65
- warnings.push("context_checkpoint_percent must be an integer from 1 through 100; ignoring it.");
69
+ catch (error) {
70
+ if (error.code !== "ENOENT") {
71
+ warnings.push(`${path}: could not read hive.yml: ${errorMessage(error)}`);
66
72
  }
73
+ return null;
67
74
  }
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 };
79
- }
80
- else {
81
- warnings.push("lead_turn_budget must contain positive integer warn and stop values, with stop greater than warn; ignoring it.");
75
+ try {
76
+ const raw = parse(text);
77
+ if (raw == null && isEmptyYamlDocument(text))
78
+ return null;
79
+ if (!isMapping(raw)) {
80
+ warnings.push(`${path}: hive.yml must be a YAML mapping.`);
81
+ return null;
82
82
  }
83
+ return raw;
83
84
  }
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;
89
- }
90
- else {
91
- warnings.push(`placement must be "split" or "window"; ignoring "${String(root.placement)}".`);
92
- }
85
+ catch (error) {
86
+ warnings.push(`${path}: hive.yml is not valid YAML: ${errorMessage(error)}`);
87
+ return null;
93
88
  }
94
- let layout = null;
95
- if (root.layout != null) {
96
- if (isWindowLayout(root.layout)) {
97
- layout = root.layout;
89
+ }
90
+ function parseProcesses(value, path, warnings) {
91
+ if (!isMapping(value)) {
92
+ warnings.push(`${path}: hive.yml \`processes\` must be a mapping of name to command.`);
93
+ return null;
94
+ }
95
+ const processes = {};
96
+ for (const [name, item] of Object.entries(value)) {
97
+ if (typeof item === "string" && item.trim() !== "") {
98
+ processes[name] = { command: item.trim(), dir: null, auto_start: true, visible: true, env: {} };
99
+ continue;
98
100
  }
99
- else {
100
- warnings.push(`layout must be one of ${WINDOW_LAYOUTS.join(", ")}; ignoring "${String(root.layout)}".`);
101
+ if (!isMapping(item)) {
102
+ warnings.push(`${path}: Process "${name}" needs a command string; skipped.`);
103
+ continue;
101
104
  }
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.");
105
+ if (typeof item.command !== "string" || item.command.trim() === "") {
106
+ warnings.push(`${path}: Process "${name}" has no command; skipped.`);
107
+ continue;
108
108
  }
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.`);
109
+ const dirValue = item.dir ?? item.working_dir;
110
+ let visible = true;
111
+ if (item.visible != null) {
112
+ if (typeof item.visible === "boolean")
113
+ visible = item.visible;
114
+ else
115
+ warnings.push(`${path}: Process "${name}": visible must be true or false; ignoring "${String(item.visible)}".`);
111
116
  }
112
- else {
113
- profile = value;
117
+ const env = {};
118
+ if (isMapping(item.env)) {
119
+ for (const [key, envValue] of Object.entries(item.env))
120
+ env[key] = String(envValue);
114
121
  }
122
+ processes[name] = {
123
+ command: item.command.trim(),
124
+ dir: typeof dirValue === "string" && dirValue.trim() !== "" ? dirValue.trim() : null,
125
+ auto_start: item.auto_start !== false,
126
+ visible,
127
+ env,
128
+ };
115
129
  }
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.");
130
+ return processes;
131
+ }
132
+ function applyLayer(root, path, source, config, sources, warnings) {
133
+ const has = (key) => Object.hasOwn(root, key);
134
+ const warn = (message) => warnings.push(`${path}: ${message}`);
135
+ const assignNullable = (key, valid, message) => {
136
+ if (!has(key))
137
+ return;
138
+ const value = root[key];
139
+ if (value == null) {
140
+ config[key] = null;
141
+ sources[key] = source;
120
142
  }
121
143
  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
- }
144
+ const normalized = valid(value);
145
+ if (normalized === undefined)
146
+ warn(message(value));
147
+ else {
148
+ config[key] = normalized;
149
+ sources[key] = source;
135
150
  }
136
- agents = valid.length > 0 ? valid : null;
137
151
  }
138
- }
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.");
152
+ };
153
+ assignNullable("lead", (value) => typeof value === "string" && value.trim() ? value.trim() : undefined, (value) => `lead must be a non-empty command string; ignoring "${String(value)}".`);
154
+ assignNullable("placement", (value) => value === "split" || value === "window" ? value : undefined, (value) => `placement must be "split" or "window"; ignoring "${String(value)}".`);
155
+ assignNullable("layout", (value) => isWindowLayout(value) ? value : undefined, (value) => `layout must be one of ${WINDOW_LAYOUTS.join(", ")}; ignoring "${String(value)}".`);
156
+ assignNullable("profile", (value) => {
157
+ const name = String(value).trim();
158
+ return name !== "" && (name === NO_PROFILE || isValidProfileName(name)) ? name : undefined;
159
+ }, (value) => typeof value === "string" && value.trim() === ""
160
+ ? "profile is empty; ignoring it."
161
+ : `profile "${String(value).trim()}" is not a valid profile name (letters, digits, dot, dash, underscore); ignoring it.`);
162
+ 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.");
163
+ if (has("lead_turn_budget")) {
164
+ const value = root.lead_turn_budget;
165
+ if (value == null) {
166
+ config.lead_turn_budget = null;
167
+ sources.lead_turn_budget = source;
168
+ }
169
+ else if (isMapping(value) && typeof value.warn === "number" && Number.isInteger(value.warn) && value.warn > 0 &&
170
+ typeof value.stop === "number" && Number.isInteger(value.stop) && value.stop > value.warn) {
171
+ config.lead_turn_budget = { warn: value.warn, stop: value.stop };
172
+ sources.lead_turn_budget = source;
143
173
  }
144
174
  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
- }
153
- }
175
+ warn("lead_turn_budget must contain positive integer warn and stop values, with stop greater than warn; ignoring it.");
154
176
  }
155
177
  }
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;
178
+ if (has("dashboard")) {
179
+ const value = root.dashboard;
180
+ if (value == null) {
181
+ config.dashboard = false;
182
+ sources.dashboard = source;
164
183
  }
165
- else {
166
- warnings.push("lead_branches must be a list of branch names; ignoring it.");
184
+ else if (typeof value === "boolean") {
185
+ config.dashboard = value;
186
+ sources.dashboard = source;
167
187
  }
188
+ else
189
+ warn(`dashboard must be true or false; ignoring "${String(value)}".`);
168
190
  }
169
- let dashboard = false;
170
- if (root.dashboard != null) {
171
- if (typeof root.dashboard === "boolean") {
172
- dashboard = root.dashboard;
191
+ if (has("agents")) {
192
+ const value = root.agents;
193
+ if (value == null) {
194
+ config.agents = null;
195
+ sources.agents = source;
173
196
  }
174
- else {
175
- warnings.push(`dashboard must be true or false; ignoring "${String(root.dashboard)}".`);
197
+ else if (Array.isArray(value)) {
198
+ const known = harnessNames();
199
+ const valid = [];
200
+ for (const entry of value) {
201
+ const name = typeof entry === "string" ? entry.trim() : "";
202
+ if (!name)
203
+ warn("agents entry must be a non-empty harness name; skipped.");
204
+ else if (!known.includes(name))
205
+ warn(`agents: "${name}" is not a known harness (${known.join(", ")}); ignoring it.`);
206
+ else
207
+ valid.push(name);
208
+ }
209
+ config.agents = valid.length ? valid : null;
210
+ sources.agents = source;
176
211
  }
212
+ else
213
+ warn("agents must be a list of harness names; ignoring it.");
177
214
  }
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.");
215
+ if (has("lead_branches")) {
216
+ const value = root.lead_branches;
217
+ if (value == null) {
218
+ config.lead_branches = null;
219
+ sources.lead_branches = source;
182
220
  }
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;
188
- }
189
- vars[key] = String(value);
190
- }
221
+ else if (Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim() !== "")) {
222
+ config.lead_branches = value.map((item) => item.trim());
223
+ sources.lead_branches = source;
191
224
  }
225
+ else
226
+ warn("lead_branches must be a list of branch names; ignoring it.");
192
227
  }
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.");
228
+ if (has("vars")) {
229
+ const value = root.vars;
230
+ if (value == null) {
231
+ for (const key of Object.keys(config.vars))
232
+ sources[`vars.${key}`] = source;
233
+ config.vars = {};
234
+ sources.vars = source;
198
235
  }
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;
236
+ else if (isMapping(value)) {
237
+ for (const [key, item] of Object.entries(value)) {
238
+ if (item == null) {
239
+ delete config.vars[key];
240
+ sources[`vars.${key}`] = source;
241
+ sources.vars = source;
213
242
  }
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
- }
243
+ else if (typeof item === "object") {
244
+ warn(`var "${key}" must be a scalar; skipped.`);
223
245
  }
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
- }
246
+ else {
247
+ config.vars[key] = String(item);
248
+ sources[`vars.${key}`] = source;
249
+ sources.vars = source;
229
250
  }
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
- };
237
251
  }
238
252
  }
253
+ else
254
+ warn("vars must be a mapping of name to value; ignoring it.");
255
+ }
256
+ if (has("processes")) {
257
+ if (source === "global")
258
+ warn("global hive.yml cannot define `processes`; only project commands can be trusted.");
259
+ else if (root.processes != null) {
260
+ const processes = parseProcesses(root.processes, path, warnings);
261
+ if (processes != null) {
262
+ config.processes = processes;
263
+ sources.processes = source;
264
+ }
265
+ }
266
+ }
267
+ for (const key of ["attach", "autoAttach"]) {
268
+ if (source === "project" && has(key))
269
+ warn(`${key} is global-only; ignoring it in project hive.yml.`);
239
270
  }
271
+ if (has("review_tags"))
272
+ warn("review_tags is no longer used by hive; remove it from hive.yml.");
273
+ }
274
+ export function resolveHiveConfig(projectPath) {
275
+ const global = readGlobalConfig();
276
+ const warnings = [...global.warnings];
277
+ const config = createBuiltInProjectYml();
278
+ const sources = Object.fromEntries(PROJECT_YML_KEYS.map((key) => [key, "built-in"]));
279
+ if (global.root)
280
+ applyLayer(global.root, globalConfigPath(), "global", config, sources, warnings);
281
+ let projectValid = false;
282
+ if (projectPath != null) {
283
+ const file = resolve(projectPath, "hive.yml");
284
+ const project = parseFile(file, warnings);
285
+ if (project) {
286
+ projectValid = true;
287
+ applyLayer(project, file, "project", config, sources, warnings);
288
+ }
289
+ }
290
+ const attachResult = resolveAttachSetting("attach", global.root, global.legacy, warnings);
291
+ const autoAttachResult = resolveAttachSetting("autoAttach", global.root, global.legacy, warnings);
240
292
  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
- },
293
+ config: global.root || projectValid ? config : null,
255
294
  warnings,
295
+ sources,
296
+ attach: attachResult,
297
+ autoAttach: autoAttachResult,
256
298
  };
257
299
  }
300
+ export function loadProjectYml(projectPath) {
301
+ return resolveHiveConfig(projectPath);
302
+ }
258
303
  export function configHash(name, command, dir, env) {
259
304
  return createHash("sha256")
260
305
  .update(JSON.stringify([name, command, dir, env]))
package/dist/result.js CHANGED
@@ -16,6 +16,23 @@ export function registrationNoticeText(notice) {
16
16
  `project_select.`);
17
17
  }
18
18
  const reportedBuilds = new Set();
19
+ const pendingNotices = new WeakMap();
20
+ function addNotice(result, text, build) {
21
+ result.content.push({ type: "text", text });
22
+ const pending = pendingNotices.get(result) ?? { texts: [] };
23
+ pending.texts.push(text);
24
+ if (build)
25
+ pending.build = build;
26
+ pendingNotices.set(result, pending);
27
+ }
28
+ export function noticeFor(result) {
29
+ return pendingNotices.get(result)?.texts.join("\n");
30
+ }
31
+ export function releaseNotice(result) {
32
+ const build = pendingNotices.get(result)?.build;
33
+ if (build)
34
+ reportedBuilds.delete(build);
35
+ }
19
36
  export async function appendRunningBuildNotice(result, session) {
20
37
  try {
21
38
  const change = runningBuildChange();
@@ -30,8 +47,8 @@ export async function appendRunningBuildNotice(result, session) {
30
47
  }
31
48
  if (reportedBuilds.has(change.disk.build_id))
32
49
  return result;
33
- result.content.push({ type: "text", text: runningBuildNotice(change, session) });
34
50
  reportedBuilds.add(change.disk.build_id);
51
+ addNotice(result, runningBuildNotice(change, session), change.disk.build_id);
35
52
  }
36
53
  catch {
37
54
  }
@@ -52,7 +69,7 @@ export async function run(fn) {
52
69
  }
53
70
  const notice = takeRegistrationNotice();
54
71
  if (notice) {
55
- result.content.push({ type: "text", text: registrationNoticeText(notice) });
72
+ addNotice(result, registrationNoticeText(notice));
56
73
  }
57
74
  return appendRunningBuildNotice(result);
58
75
  }
package/dist/scheduler.js CHANGED
@@ -1460,6 +1460,12 @@ function watchedStates(timer, snapshot) {
1460
1460
  };
1461
1461
  });
1462
1462
  }
1463
+ function idleConditionMet(timer, snapshot) {
1464
+ const states = watchedStates(timer, snapshot);
1465
+ return timer.kind === "idle_any"
1466
+ ? states.some((s) => s.gone || (s.idle && s.since != null && s.since >= timer.created_at))
1467
+ : states.length > 0 && states.every((s) => s.idle);
1468
+ }
1463
1469
  async function maybeFireIdle(timer, snapshot, now, choices) {
1464
1470
  const timedOut = timer.max_wait_at != null && timer.max_wait_at <= now;
1465
1471
  if (isStandingWatch(timer)) {
@@ -1476,24 +1482,24 @@ async function maybeFireIdle(timer, snapshot, now, choices) {
1476
1482
  return;
1477
1483
  }
1478
1484
  let ready = false;
1485
+ let idleMet = false;
1479
1486
  if (timedOut) {
1480
1487
  ready = true;
1488
+ if (snapshot !== null)
1489
+ idleMet = idleConditionMet(timer, snapshot);
1481
1490
  }
1482
1491
  else {
1483
1492
  if (snapshot === null)
1484
1493
  return;
1485
- const states = watchedStates(timer, snapshot);
1486
- ready =
1487
- timer.kind === "idle_any"
1488
- ? states.some((s) => s.gone || (s.idle && s.since != null && s.since >= timer.created_at))
1489
- : states.length > 0 && states.every((s) => s.idle);
1494
+ idleMet = idleConditionMet(timer, snapshot);
1495
+ ready = idleMet;
1490
1496
  if (!ready)
1491
1497
  noteBlockedWatched(timer, snapshot, choices);
1492
1498
  }
1493
1499
  if (ready) {
1494
1500
  const decision = deliverable(timer, snapshot, choices);
1495
1501
  if (decision.ok && claimOneShot(timer)) {
1496
- await deliver(timer, timedOut ? "max wait reached" : "", choices, decision.typedSeen, decision.firstHeldAt);
1502
+ await deliver(timer, timedOut && !idleMet ? "max wait reached" : "", choices, decision.typedSeen, decision.firstHeldAt);
1497
1503
  }
1498
1504
  }
1499
1505
  }
package/dist/spawn.js CHANGED
@@ -16,6 +16,17 @@ export function withWindowClaim(claim) {
16
16
  }
17
17
  return db.transaction(claim).immediate();
18
18
  }
19
+ export function storedLaunchOptions(value) {
20
+ if (value == null)
21
+ return { args: [], readOnly: false };
22
+ const stored = JSON.parse(value);
23
+ if (Array.isArray(stored))
24
+ return { args: stored, readOnly: false };
25
+ if (stored?.read_only === true && Array.isArray(stored.args)) {
26
+ return { args: stored.args, readOnly: true };
27
+ }
28
+ throw new Error("Invalid stored worker launch options");
29
+ }
19
30
  export function splitTargetWindow(session, projectId, parentActor) {
20
31
  const parent = db
21
32
  .prepare("SELECT tmux_target, tmux_socket FROM agents WHERE actor_id = ? AND status = 'running' ORDER BY id DESC LIMIT 1")
@@ -30,7 +41,7 @@ export function splitTargetWindow(session, projectId, parentActor) {
30
41
  export function buildEnvFlags(env) {
31
42
  return Object.entries(env).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
32
43
  }
33
- function agentIdentityEnv(actorId, name, projectPath) {
44
+ export function agentIdentityEnv(actorId, name, projectPath) {
34
45
  return {
35
46
  HIVE_AGENT_ID: actorId,
36
47
  HIVE_AGENT_NAME: name,
@@ -152,9 +163,11 @@ export function launchAgent(spec) {
152
163
  let info;
153
164
  try {
154
165
  info = db
155
- .prepare("INSERT INTO agents (project_id, name, command, cwd, kind, parent_actor_id, tmux_socket, session_id, codex_home) " +
156
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
157
- .run(spec.projectId, spec.name, typeof spec.commandString === "string" ? spec.commandString : "", spec.cwd, spec.kind, spec.parentActor, socket, spec.sessionId ?? "", spec.codexHome ?? "");
166
+ .prepare("INSERT INTO agents (project_id, name, command, cwd, kind, parent_actor_id, tmux_socket, session_id, codex_home, model, extra_args) " +
167
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
168
+ .run(spec.projectId, spec.name, typeof spec.commandString === "string" ? spec.commandString : "", spec.cwd, spec.kind, spec.parentActor, socket, spec.sessionId ?? "", spec.codexHome ?? "", spec.model ?? null, spec.readOnly
169
+ ? JSON.stringify({ args: spec.extraArgs ?? [], read_only: true })
170
+ : spec.extraArgs ? JSON.stringify(spec.extraArgs) : null);
158
171
  }
159
172
  catch (e) {
160
173
  throw asNameClash(e, spec.name);
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { noticeFor, releaseNotice } from "./result.js";
2
3
  function isBuiltSchema(value) {
3
4
  return typeof value === "object" && value !== null && ("_zod" in value || "_def" in value);
4
5
  }
@@ -14,11 +15,15 @@ function strictSchemaFor(toolName, inputSchema) {
14
15
  }
15
16
  // The SDK requires structuredContent whenever outputSchema is declared but never populates it itself,
16
17
  // so this stays scoped to declaring tools rather than attached blanket in src/result.ts's ok().
18
+ // Claude Code shows the model structuredContent and drops content for these tools, so a notice
19
+ // appended to content reaches it only as structuredContent.hive_notice.
17
20
  function withStructuredContent(name, cb) {
18
21
  return async (...args) => {
19
22
  const result = (await cb(...args));
20
- if (result.isError || result.structuredContent)
23
+ if (result.isError)
21
24
  return result;
25
+ if (result.structuredContent)
26
+ return withNotice(result);
22
27
  const first = result.content[0];
23
28
  const text = first?.type === "text" ? first.text : undefined;
24
29
  let parsed;
@@ -30,8 +35,9 @@ function withStructuredContent(name, cb) {
30
35
  }
31
36
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
32
37
  result.structuredContent = parsed;
33
- return result;
38
+ return withNotice(result);
34
39
  }
40
+ releaseNotice(result);
35
41
  return {
36
42
  content: [
37
43
  {
@@ -45,8 +51,23 @@ function withStructuredContent(name, cb) {
45
51
  };
46
52
  };
47
53
  }
54
+ function withNotice(result) {
55
+ const notice = noticeFor(result);
56
+ if (notice && result.structuredContent)
57
+ result.structuredContent = { ...result.structuredContent, hive_notice: notice };
58
+ return result;
59
+ }
60
+ function withNoticeField(outputSchema) {
61
+ return outputSchema === undefined || isBuiltSchema(outputSchema)
62
+ ? outputSchema
63
+ : { ...outputSchema, hive_notice: z.string().optional() };
64
+ }
48
65
  export function enforceStrictInput(server) {
49
66
  const register = server.registerTool.bind(server);
50
- server.registerTool = ((name, config, cb) => register(name, { ...config, inputSchema: strictSchemaFor(name, config.inputSchema) }, (config.outputSchema ? withStructuredContent(name, cb) : cb)));
67
+ server.registerTool = ((name, config, cb) => register(name, {
68
+ ...config,
69
+ inputSchema: strictSchemaFor(name, config.inputSchema),
70
+ outputSchema: withNoticeField(config.outputSchema),
71
+ }, (config.outputSchema ? withStructuredContent(name, cb) : cb)));
51
72
  return server;
52
73
  }