@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/claude-plugin/README.md +3 -0
- 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/build-info.json +1 -1
- package/dist/cli.js +77 -58
- package/dist/codexHome.js +23 -8
- package/dist/config.js +8 -61
- package/dist/db.js +20 -3
- package/dist/globalConfig.js +247 -0
- package/dist/harnesses.js +8 -0
- package/dist/help.js +1 -1
- package/dist/hooks.js +2 -1
- package/dist/kickoff.js +3 -4
- package/dist/projectYml.js +224 -179
- package/dist/scheduler.js +12 -6
- package/dist/spawn.js +17 -4
- package/dist/tools/agents.js +149 -15
- package/dist/tools/kv.js +24 -0
- package/dist/tools/leases.js +12 -0
- package/dist/tools/meta.js +42 -0
- package/dist/tools/pads.js +42 -0
- package/dist/tools/todos.js +54 -0
- package/dist/tools/wakes.js +36 -0
- package/package.json +1 -1
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,
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
197
|
-
`HIVE_AGENT_ID = ${tomlString(input.actorId)}`,
|
|
198
|
-
"",
|
|
212
|
+
...mcpEnvTable(input.serverEnv),
|
|
199
213
|
// Only accepted under [tui]; a top-level status_line is an unknown field to --strict-config.
|
|
200
214
|
`[tui]`,
|
|
201
215
|
`status_line = ${tomlStringArray(input.statusLine)}`,
|
|
202
216
|
].join("\n") + "\n");
|
|
203
217
|
}
|
|
204
218
|
// Pure on purpose: agent_resume reuses it against an EXISTING home, so it must not write.
|
|
205
|
-
export function codexLaunchArgs(cwd) {
|
|
219
|
+
export function codexLaunchArgs(cwd, readOnly = false) {
|
|
206
220
|
const root = gitPrimaryRoot(cwd);
|
|
207
221
|
const commonDir = root ? join(root, ".git") : null;
|
|
208
222
|
return [
|
|
209
223
|
// Declining review of hive's OWN generated hooks.json, not a stranger's - see the reference.
|
|
210
224
|
"--dangerously-bypass-hook-trust",
|
|
211
225
|
// NOT parity with claude's posture, and --add-dir is NOT containment - see the reference.
|
|
212
|
-
"--dangerously-bypass-approvals-and-sandbox",
|
|
226
|
+
...(readOnly ? ["--sandbox", "read-only", "-c", 'approval_policy="never"'] : ["--dangerously-bypass-approvals-and-sandbox"]),
|
|
213
227
|
// Inert alongside the flag above, kept because its value seeds [projects.<root>] trust.
|
|
214
|
-
...(commonDir ? ["--add-dir", commonDir] : []),
|
|
228
|
+
...(commonDir && !readOnly ? ["--add-dir", commonDir] : []),
|
|
215
229
|
];
|
|
216
230
|
}
|
|
217
231
|
export function ensureCodexHooksFile(key, options) {
|
|
@@ -287,11 +301,12 @@ export function ensureCodexHome(input) {
|
|
|
287
301
|
projectRoot,
|
|
288
302
|
nodeBin: process.execPath,
|
|
289
303
|
indexJs,
|
|
290
|
-
|
|
304
|
+
serverEnv: { ...input.serverEnv, HIVE_AGENT_ID: input.actorId },
|
|
291
305
|
statusLine,
|
|
306
|
+
readOnly: input.readOnly,
|
|
292
307
|
modelContextWindow,
|
|
293
308
|
modelAutoCompactTokenLimit,
|
|
294
309
|
}));
|
|
295
310
|
const instructionLayers = [...(hasGlobal ? ["global"] : []), ...(localOverride ? ["local"] : [])];
|
|
296
|
-
return { extraArgs: codexLaunchArgs(input.cwd), hooksWired, instructionLayers };
|
|
311
|
+
return { extraArgs: codexLaunchArgs(input.cwd, input.readOnly), hooksWired, instructionLayers };
|
|
297
312
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,72 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { storeDir } from "./dataDir.js";
|
|
4
|
-
export const ATTACH_MODES = ["auto", "raw", "control"];
|
|
5
|
-
export function isAttachMode(value) {
|
|
6
|
-
return ATTACH_MODES.includes(value);
|
|
7
|
-
}
|
|
8
|
-
export const AUTO_ATTACH_MODES = ["auto", "on", "off"];
|
|
9
|
-
export function isAutoAttach(value) {
|
|
10
|
-
return AUTO_ATTACH_MODES.includes(value);
|
|
11
|
-
}
|
|
12
|
-
function configPath() {
|
|
13
|
-
return join(storeDir(), "config.json");
|
|
14
|
-
}
|
|
15
|
-
function readConfig() {
|
|
16
|
-
const path = configPath();
|
|
17
|
-
let raw;
|
|
18
|
-
try {
|
|
19
|
-
raw = readFileSync(path, "utf8");
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return {};
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const parsed = JSON.parse(raw);
|
|
26
|
-
return parsed !== null && typeof parsed === "object" ? parsed : {};
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function envAttachMode() {
|
|
33
|
-
const value = process.env.HIVE_ATTACH_MODE;
|
|
34
|
-
return isAttachMode(value) ? value : null;
|
|
35
|
-
}
|
|
36
|
-
function envAutoAttach() {
|
|
37
|
-
const value = process.env.HIVE_AUTO_ATTACH;
|
|
38
|
-
if (value === "0")
|
|
39
|
-
return "off";
|
|
40
|
-
return isAutoAttach(value) ? value : null;
|
|
41
|
-
}
|
|
1
|
+
import { readGlobalConfig, resolveAttachSetting, writeGlobalConfigKey } from "./globalConfig.js";
|
|
2
|
+
export { ATTACH_MODES, AUTO_ATTACH_MODES, isAttachMode, isAutoAttach, migrateLegacyConfig, } from "./globalConfig.js";
|
|
42
3
|
export function resolvedAttachMode() {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
return { mode: fromEnv, source: "env" };
|
|
46
|
-
const value = readConfig().attach;
|
|
47
|
-
return isAttachMode(value) ? { mode: value, source: "config" } : { mode: "auto", source: "detection" };
|
|
4
|
+
const global = readGlobalConfig();
|
|
5
|
+
return resolveAttachSetting("attach", global.root, global.legacy, global.warnings);
|
|
48
6
|
}
|
|
49
7
|
export function attachMode() {
|
|
50
8
|
return resolvedAttachMode().mode;
|
|
51
9
|
}
|
|
52
10
|
export function resolvedAutoAttach() {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
return { value: fromEnv, source: "env" };
|
|
56
|
-
const value = readConfig().autoAttach;
|
|
57
|
-
return isAutoAttach(value)
|
|
58
|
-
? { value, source: "config" }
|
|
59
|
-
: { value: "auto", source: "detection" };
|
|
11
|
+
const global = readGlobalConfig();
|
|
12
|
+
return resolveAttachSetting("autoAttach", global.root, global.legacy, global.warnings);
|
|
60
13
|
}
|
|
61
14
|
export function setAttachMode(mode) {
|
|
62
|
-
|
|
63
|
-
config.attach = mode;
|
|
64
|
-
mkdirSync(storeDir(), { recursive: true });
|
|
65
|
-
writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}\n`);
|
|
15
|
+
writeGlobalConfigKey("attach", mode);
|
|
66
16
|
}
|
|
67
17
|
export function setAutoAttach(value) {
|
|
68
|
-
|
|
69
|
-
config.autoAttach = value;
|
|
70
|
-
mkdirSync(storeDir(), { recursive: true });
|
|
71
|
-
writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}\n`);
|
|
18
|
+
writeGlobalConfigKey("autoAttach", value);
|
|
72
19
|
}
|
package/dist/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 (
|
|
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
|
-
|
|
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
|
});
|