@cmgmyr/hive 1.2.2 → 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.
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { pathToFileURL } from "node:url";
9
9
  import { checkAbi, describeAbi, describeInterpreter, nodeRangeForNodeApi, requiredNodeApi } from "./abi.js";
10
10
  import { versionInfo } from "./version.js";
11
11
  import { claudeConfigDir } from "./claudeDir.js";
12
- import { ATTACH_MODES, AUTO_ATTACH_MODES, attachMode, isAttachMode, isAutoAttach, resolvedAttachMode, resolvedAutoAttach, setAttachMode, setAutoAttach, } from "./config.js";
12
+ import { ATTACH_MODES, AUTO_ATTACH_MODES, attachMode, isAttachMode, isAutoAttach, migrateLegacyConfig, resolvedAutoAttach, setAttachMode, setAutoAttach, } from "./config.js";
13
13
  import { cliPath, dispatcherDir, dispatcherPath, dispatcherScript, durabilityLines, firstHiveOnPath, linkedWorktreePin, pathAdvice, readDispatcher, } from "./dispatcher.js";
14
14
  import { codexConfigPath, codexHiveRegistrations, codexRegistrationProblem, hiveRegistrations, registrationOffer, registrationProblem, } from "./mcpConfig.js";
15
15
  import { DEFAULT_DATA_DIR } from "./dataDir.js";
@@ -19,7 +19,7 @@ import { checkoutUpgradeSteps, detectInstallShape, globalUpgradeSteps } from "./
19
19
  import { isLowHeadroom, orphanLoginShellDetails, ptyHeadroom } from "./ptys.js";
20
20
  import { addProject, agentProjectPin, currentActor, effectiveProjectId, findProjectForCwd, getProjectByPath, gitPrimaryRoot, getProject, listProjects, registeredAncestor, takeRegistrationNotice, } from "./context.js";
21
21
  import { ensureHooksFile } from "./hooks.js";
22
- import { errorMessage, parseTags, registrationNoticeText, withTrailingNewline } from "./result.js";
22
+ import { errorMessage, registrationNoticeText, withTrailingNewline } from "./result.js";
23
23
  import { ACTIVE_TIMER_WHERE, dashboardFileContained, describeStall, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, transcriptStaleness, wasHeldForPaneReissue, } from "./scheduler.js";
24
24
  import { readTurnCount } from "./turnCount.js";
25
25
  import { STALL_BOUND_SECONDS } from "./backgroundTasks.js";
@@ -30,7 +30,7 @@ import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchA
30
30
  import { adoptableWindow, applyLayout, claimInitialWindow, configureHiveWindow, controlModeFor, createWindow, DEFAULT_LAYOUT, describePaneChoice, ensureSession, findProjectWindow, foreignSocket, isPaneTarget, isViewSessionName, listOwnedWindows, ORPHAN_MIN_AGE_MS, orphanScratchServers, orphansWorthWarningAbout, paneIsAloneInWindow, paneVisibility, panePid, PROCESSES_LAYOUT, processesPaneTitle, processesWindowName, projectWindows, RAW_ATTACH_TMUX_CONFIG, renderAttachCommand, resolveAttachTarget, resolveInTmuxTarget, paneReissued, rowLive, rowLiveProbe, SESSION_PREFIX, appendGlobalHook, globalHooks, replaceGlobalHooks, sessionName, setPaneTitle, shellQuote, shownPaneTitle, makeProcessesWindow, tmux, TMUX_DOC, tmuxSaysNothingThere, tmuxTimeoutOverride, tmuxSocketPath, untrustedTmuxServer, waitForPaneEstablished, windowLayout, } from "./tmux.js";
31
31
  import { describeVisibility, processCounts } from "./dashboard.js";
32
32
  import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, STOP_REASONS, } from "./processes.js";
33
- import { activeProfile, agentVarKeys, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, resolveCommandDir, } from "./projectYml.js";
33
+ import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, } from "./projectYml.js";
34
34
  import { writeProjectPosture } from "./brief.js";
35
35
  import { carriesNameFlag, harnessFor, hasTranscriptSignal, paneClassifierFor, transcriptDirFor } from "./harnesses.js";
36
36
  import { codexHomeDir, codexInstructionsPhrase, ensureCodexHome, reapCodexHome } from "./codexHome.js";
@@ -38,7 +38,7 @@ import { TRIAGE_MESSAGE } from "./kickoff.js";
38
38
  import { ageSecondsSince, deriveProvenance, describeForHuman, describeLastLogEvent, humanizeAge, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "./stateProvenance.js";
39
39
  import { awaitingFirstPromptSql } from "./firstPrompt.js";
40
40
  import { checkoutRoot, createProfile, forkProfile, PROFILE_FILES, ProfileError, profileExists, profileFileNames, profileNames, profileStatus, readProfileFile, referencedPads, referencedPaths, renderProfileFile, resolveProfileFile, REWRITE_THRESHOLD, templateVars, userProfilesDir, } from "./profiles.js";
41
- import { COMMENT_COUNT_SQL, getTodoDetail, listTodoSummaries, OPEN_BLOCKERS_SQL, TODO_STATUSES, } from "./tools/todos.js";
41
+ import { getTodoDetail, listTodoSummaries, OPEN_BLOCKERS_SQL, TODO_STATUSES, } from "./tools/todos.js";
42
42
  import { createPad, getActivePadByName, listActivePads, overwritePadContent, } from "./tools/pads.js";
43
43
  function usage() {
44
44
  console.log(`hive — run a crew of Claude Code and Codex workers from one lead session
@@ -311,6 +311,12 @@ function ensureLeadRow(project, command) {
311
311
  casExpected: "",
312
312
  };
313
313
  }
314
+ function reportMigrationResult(result) {
315
+ if (result.notice)
316
+ console.error(result.notice);
317
+ for (const warning of result.warnings)
318
+ console.error(`! ${warning}`);
319
+ }
314
320
  async function cmdLead(argv) {
315
321
  const unknownFlag = argv.find((a) => a.startsWith("--") && a !== "--no-dashboard");
316
322
  if (unknownFlag !== undefined) {
@@ -324,6 +330,7 @@ async function cmdLead(argv) {
324
330
  const project = resolveProject(path, (text) => {
325
331
  registrationNotice = text;
326
332
  });
333
+ reportMigrationResult(migrateLegacyConfig());
327
334
  const session = sessionName();
328
335
  const hooksPath = ensureHooksFile();
329
336
  const { config, warnings } = loadProjectYml(project.path);
@@ -387,6 +394,14 @@ async function cmdLead(argv) {
387
394
  // actually starts under it) and correct regardless of whether this invocation ends up creating
388
395
  // a pane or adopting one. Which home ends up orphaned - this one, or the previous invocation's -
389
396
  // is decided after createdPane is known, below.
397
+ const leadEnv = {
398
+ HIVE_AGENT_ID: leadActorId,
399
+ HIVE_AGENT_NAME: LEAD_NAME,
400
+ HIVE_LEAD: "1",
401
+ HIVE_DATA_DIR: dataDir,
402
+ HIVE_PROJECT_LOCK: "",
403
+ HIVE_PROJECT_PATH: "",
404
+ };
390
405
  let newCodexHomeKey;
391
406
  if (leadHarness.needsHome) {
392
407
  newCodexHomeKey = randomUUID();
@@ -401,6 +416,7 @@ async function cmdLead(argv) {
401
416
  const home = ensureCodexHome({
402
417
  key: newCodexHomeKey,
403
418
  actorId: leadActorId,
419
+ serverEnv: leadEnv,
404
420
  cwd: project.path,
405
421
  brief: renderedPosture ?? "",
406
422
  lead: true,
@@ -425,12 +441,7 @@ async function cmdLead(argv) {
425
441
  (instructionsPhrase ? ` (${instructionsPhrase})` : ""));
426
442
  }
427
443
  const envFlags = buildEnvFlags({
428
- HIVE_AGENT_ID: leadActorId,
429
- HIVE_AGENT_NAME: LEAD_NAME,
430
- HIVE_LEAD: "1",
431
- HIVE_DATA_DIR: dataDir,
432
- HIVE_PROJECT_LOCK: "",
433
- HIVE_PROJECT_PATH: "",
444
+ ...leadEnv,
434
445
  ...(newCodexHomeKey ? { CODEX_HOME: codexHomeDir(newCodexHomeKey) } : {}),
435
446
  });
436
447
  const { leadPane, leadWindow, createdPane } = withWindowClaim(() => {
@@ -553,7 +564,7 @@ const HIVE_YML_TEMPLATE = `# hive project config. Read by \`hive lead\` from the
553
564
  # Commands defined here run only after a one-time interactive approval,
554
565
  # and re-require it whenever they change.
555
566
 
556
- placement: split # placement for workers and visible processes: split (panes) or
567
+ # placement: split # placement for workers and visible processes: split (panes) or
557
568
  # window (tabs)
558
569
 
559
570
  # layout: main-vertical # pane arrangement for placement: split.
@@ -573,11 +584,6 @@ placement: split # placement for workers and visible processes: s
573
584
 
574
585
  # dashboard: true # write .hive/dashboard.html (default: false)
575
586
 
576
- # review_tags: [from-review] # todo tags \`hive doctor\` counts as review findings and
577
- # reports as triaged (has a comment, completed, or archived)
578
- # or untriaged. A tag also matches its own suffixed rounds.
579
- # Absent means it tracks none.
580
-
581
587
  # vars: # substituted into the profile runbook
582
588
  # repo: owner/name # {{repo}}
583
589
  # ticket_prefix: DEVX # sections needing it drop when it is unset
@@ -729,6 +735,7 @@ async function cmdInit(argv) {
729
735
  rejectUnknownFlags("init", parsed, "--profile <name> and --no-profile");
730
736
  requireFlagValues("init", parsed);
731
737
  const noProfile = parsed.flags.has("--no-profile");
738
+ const profileFlag = noProfile || parsed.values.has("--profile");
732
739
  const path = parsed.positional[0];
733
740
  let chosen = noProfile ? NO_PROFILE : null;
734
741
  if (parsed.values.has("--profile")) {
@@ -789,21 +796,32 @@ async function cmdInit(argv) {
789
796
  if (path == null && newlyRegistered)
790
797
  console.error(registrationNoticeText(project));
791
798
  const ymlPath = join(project.path, "hive.yml");
792
- const already = existsSync(ymlPath) ? loadProjectYml(project.path).config?.profile ?? null : null;
799
+ const ymlExists = existsSync(ymlPath);
800
+ const loaded = loadProjectYml(project.path);
801
+ const projectProfileSet = ymlExists && loaded.sources.profile === "project";
802
+ const already = projectProfileSet ? loaded.config?.profile ?? null : null;
803
+ let inheritedProfile = false;
793
804
  if (already != null) {
794
805
  if (chosen != null && chosen !== already) {
795
806
  console.log(`- hive.yml: already set to "profile: ${already}"; edit it by hand to change it`);
796
807
  }
797
808
  chosen = already;
798
809
  }
810
+ else if (projectProfileSet && !profileFlag) {
811
+ chosen = null;
812
+ }
813
+ else if (!projectProfileSet && !profileFlag && loaded.config?.profile != null) {
814
+ chosen = loaded.config.profile;
815
+ inheritedProfile = loaded.sources.profile === "global";
816
+ }
799
817
  else if (chosen == null && process.stdin.isTTY && process.stdout.isTTY) {
800
818
  chosen = await askForProfile();
801
819
  }
802
- if (!existsSync(ymlPath)) {
803
- writeFileSync(ymlPath, HIVE_YML_TEMPLATE + (chosen ? `\nprofile: ${chosen}\n` : ""));
820
+ if (!ymlExists) {
821
+ writeFileSync(ymlPath, HIVE_YML_TEMPLATE + (chosen && !inheritedProfile ? `\nprofile: ${chosen}\n` : ""));
804
822
  console.log(`- hive.yml: created${chosen ? ` with profile: ${chosen}` : ""}`);
805
823
  }
806
- else if (already == null && chosen != null) {
824
+ else if (already == null && chosen != null && !inheritedProfile) {
807
825
  writeProfileKey(ymlPath, chosen);
808
826
  console.log(`- hive.yml: added profile: ${chosen}`);
809
827
  }
@@ -1462,7 +1480,6 @@ const verboseInfo = (_check, label, ...lines) => {
1462
1480
  if (doctorVerbose)
1463
1481
  report("info", label, lines);
1464
1482
  };
1465
- const isReviewFindingTag = (bases, tag) => bases.some((base) => tag === base || tag.startsWith(`${base}-`));
1466
1483
  function cmdUpgrade(argv) {
1467
1484
  const parsed = parseArgs(argv, { flags: ["--check", "--run"] });
1468
1485
  rejectUnknownFlags("upgrade", parsed, "--check, --run");
@@ -1576,6 +1593,7 @@ function cmdSetup(argv) {
1576
1593
  console.log("Or pin this worktree anyway: hive setup --force");
1577
1594
  process.exit(1);
1578
1595
  }
1596
+ reportMigrationResult(migrateLegacyConfig());
1579
1597
  mkdirSync(dir, { recursive: true });
1580
1598
  writeFileSync(file, dispatcherScript(node, cli));
1581
1599
  chmodSync(file, 0o755);
@@ -2023,6 +2041,33 @@ function reportStalledWorkers(projectId) {
2023
2041
  "worker does not reliably remember what it was doing (.claude/rules/worker-state.md).");
2024
2042
  }
2025
2043
  }
2044
+ function reportEffectiveConfig(loaded) {
2045
+ const config = loaded.config ?? BUILT_IN_PROJECT_YML;
2046
+ for (const key of PROJECT_YML_KEYS) {
2047
+ info(`config ${key}`, `${JSON.stringify(config[key])} (source: ${loaded.sources[key] ?? "built-in"})`);
2048
+ }
2049
+ const varKeys = new Set(Object.keys(config.vars));
2050
+ for (const key of Object.keys(loaded.sources)) {
2051
+ if (key.startsWith("vars."))
2052
+ varKeys.add(key.slice("vars.".length));
2053
+ }
2054
+ for (const key of [...varKeys].sort()) {
2055
+ const value = Object.hasOwn(config.vars, key) ? config.vars[key] : null;
2056
+ info(`config vars.${key}`, `${JSON.stringify(value)} (source: ${loaded.sources[`vars.${key}`] ?? "built-in"})`);
2057
+ }
2058
+ const { mode: attachMode, source: attachSource } = loaded.attach;
2059
+ info("attach mode", attachSource === "env"
2060
+ ? `${attachMode} (HIVE_ATTACH_MODE override; testing only, does not reach auto-attach; source: env)`
2061
+ : attachSource === "global"
2062
+ ? `${attachMode} (set with \`hive setup --attach\`; source: global)`
2063
+ : `${attachMode} (default; set with \`hive setup --attach\`; source: built-in)`);
2064
+ const { value: autoAttach, source: autoAttachSource } = loaded.autoAttach;
2065
+ info("auto-attach", autoAttachSource === "env"
2066
+ ? `${autoAttach} (HIVE_AUTO_ATTACH override; testing only; source: env)`
2067
+ : autoAttachSource === "global"
2068
+ ? `${autoAttach} (set with \`hive setup --auto-attach\`; source: global)`
2069
+ : `${autoAttach} (default; set with \`hive setup --auto-attach\`; source: built-in)`);
2070
+ }
2026
2071
  function cmdDoctor(argv) {
2027
2072
  const strict = argv.includes("--strict");
2028
2073
  doctorVerbose = argv.includes("--verbose");
@@ -2032,6 +2077,7 @@ function cmdDoctor(argv) {
2032
2077
  console.error(`hive doctor: unknown argument "${unknown}". Flags are --strict and --verbose.`);
2033
2078
  process.exit(1);
2034
2079
  }
2080
+ reportMigrationResult(migrateLegacyConfig());
2035
2081
  let failures = 0;
2036
2082
  const fail = (label, ...lines) => {
2037
2083
  failures += 1;
@@ -2089,9 +2135,15 @@ function cmdDoctor(argv) {
2089
2135
  reportSessionInterpreters();
2090
2136
  reportCodexHomes(here?.id ?? null);
2091
2137
  const loaded = loadProjectYml(here?.path ?? process.cwd());
2092
- for (const w of loaded.warnings)
2093
- warn("hive.yml", w);
2138
+ for (const w of loaded.warnings) {
2139
+ const qualified = /^([^:]+): ([\s\S]*)$/.exec(w);
2140
+ if (qualified)
2141
+ warn(qualified[1], qualified[2]);
2142
+ else
2143
+ warn("hive.yml", w);
2144
+ }
2094
2145
  const config = loaded.config;
2146
+ reportEffectiveConfig(loaded);
2095
2147
  const profile = activeProfile(config);
2096
2148
  if (here && existsSync(join(here.path, ".claude", "dashboard", "index.html"))) {
2097
2149
  info("dashboard", "an older hive wrote .claude/dashboard/index.html; hive now writes .hive/dashboard.html. Delete the old file and directory.");
@@ -2399,20 +2451,7 @@ function cmdDoctor(argv) {
2399
2451
  return health.message;
2400
2452
  });
2401
2453
  {
2402
- const { value, source } = resolvedAutoAttach();
2403
- info("auto-attach", source === "env"
2404
- ? `${value} (HIVE_AUTO_ATTACH override; testing only)`
2405
- : source === "config"
2406
- ? `${value} (set with \`hive setup --auto-attach\`)`
2407
- : `${value} (default; set with \`hive setup --auto-attach\`)`);
2408
- }
2409
- {
2410
- const { mode, source } = resolvedAttachMode();
2411
- info("attach mode", source === "env"
2412
- ? `${mode} (HIVE_ATTACH_MODE override; testing only, does not reach auto-attach)`
2413
- : source === "config"
2414
- ? `${mode} (set with \`hive setup --attach\`)`
2415
- : `${mode} (default; set with \`hive setup --attach\`)`);
2454
+ const { mode } = loaded.attach;
2416
2455
  if (mode === "raw") {
2417
2456
  try {
2418
2457
  const owned = tmux("list-windows", "-a", "-F", "#{session_name}:#{window_id}\t#{@hive-owned}")
@@ -2432,8 +2471,7 @@ function cmdDoctor(argv) {
2432
2471
  `monitor-bell ${option("-w", "monitor-bell")}`);
2433
2472
  }
2434
2473
  }
2435
- catch {
2436
- }
2474
+ catch { }
2437
2475
  }
2438
2476
  }
2439
2477
  if (here) {
@@ -2444,25 +2482,6 @@ function cmdDoctor(argv) {
2444
2482
  info("processes", `${running} running (${located}), ${notStarted} defined not running`);
2445
2483
  }
2446
2484
  }
2447
- const reviewTags = config?.review_tags ?? [];
2448
- // Never name hive.yml here: doctor stays silent about that file when nothing is wrong with it
2449
- // (test/config-warnings.test.mjs), and this line prints on every ordinary run.
2450
- if (here && reviewTags.length === 0) {
2451
- info("review findings", "no review_tags configured; nothing to track.");
2452
- }
2453
- else if (here) {
2454
- const findings = db
2455
- .prepare(`SELECT id, tags, status, archived_at, ${COMMENT_COUNT_SQL} AS comment_count
2456
- FROM todos t WHERE t.project_id = ?`)
2457
- .all(here.id).filter((t) => parseTags(t.tags).some((tag) => isReviewFindingTag(reviewTags, tag)));
2458
- const untriaged = findings.filter((t) => t.comment_count === 0 && t.status !== "completed" && t.archived_at == null);
2459
- info("review findings", `${findings.length} tracked (tagged ${reviewTags.join(", ")}): ` +
2460
- `${findings.length - untriaged.length} triaged, ${untriaged.length} untriaged.`);
2461
- if (untriaged.length > 0) {
2462
- warn("review findings", `${untriaged.length} untriaged: ${untriaged.map((t) => `todo ${t.id}`).join(", ")}.`, "Triage means a comment recording a decision, or completed/archived - comment with the outcome " +
2463
- "(dispatched, rejected and why, etc.) on each before closing this wave.");
2464
- }
2465
- }
2466
2485
  const problems = failures + (strict ? gatingWarnings : 0);
2467
2486
  const tail = strict
2468
2487
  ? `${warnings} warning(s), ${gatingWarnings} promoted by --strict.`
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
- `[mcp_servers.hive.env]`,
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
- actorId: input.actorId,
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 { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
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 fromEnv = envAttachMode();
44
- if (fromEnv)
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 fromEnv = envAutoAttach();
54
- if (fromEnv)
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
- const config = readConfig();
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
- const config = readConfig();
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/db.js CHANGED
@@ -368,6 +368,10 @@ 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;
371
375
  `,
372
376
  ];
373
377
  export function storeSchemaAhead(database) {
@@ -392,12 +396,17 @@ function retryOnBusy(fn, label) {
392
396
  }
393
397
  catch (e) {
394
398
  const busy = e instanceof Error && (e.code === "SQLITE_BUSY" || /database is locked/.test(e.message));
395
- if (!busy || attempt >= MIGRATE_LOCK_MAX_ATTEMPTS) {
399
+ if (busy && attempt < MIGRATE_LOCK_MAX_ATTEMPTS)
400
+ continue;
401
+ if (busy) {
396
402
  console.error(`hive: ${label} did not complete after ${attempt} attempt(s): the store's write lock never freed ` +
397
403
  "up. Another hive process is likely still applying a migration (or is stuck); check for a " +
398
404
  `wedged hive process and try again. Underlying error: ${errorMessage(e)}`);
399
- process.exit(1);
400
405
  }
406
+ else {
407
+ console.error(`hive: ${label} failed: ${errorMessage(e)}`);
408
+ }
409
+ process.exit(1);
401
410
  }
402
411
  }
403
412
  }
@@ -424,7 +433,15 @@ export function migrate() {
424
433
  const version = i + 1;
425
434
  if (applied.has(version))
426
435
  return;
427
- db.exec(sql);
436
+ try {
437
+ db.exec(sql);
438
+ }
439
+ catch (e) {
440
+ const wrapped = new Error(`migration ${version}: ${errorMessage(e)}`);
441
+ if (e instanceof Error)
442
+ wrapped.code = e.code;
443
+ throw wrapped;
444
+ }
428
445
  db.prepare("INSERT INTO migrations (version) VALUES (?)").run(version);
429
446
  });
430
447
  });