@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.
- package/README.md +1 -1
- 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 +78 -59
- 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 +2 -2
- package/dist/hooks.js +2 -1
- package/dist/kickoff.js +3 -4
- package/dist/projectYml.js +224 -179
- package/dist/result.js +19 -2
- package/dist/scheduler.js +12 -6
- package/dist/spawn.js +17 -4
- package/dist/strictInput.js +24 -3
- 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 +2 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { isMap, parse, parseDocument } from "yaml";
|
|
5
|
+
import { storeDir } from "./dataDir.js";
|
|
6
|
+
import { errorMessage } from "./result.js";
|
|
7
|
+
export const ATTACH_MODES = ["auto", "raw", "control"];
|
|
8
|
+
export const AUTO_ATTACH_MODES = ["auto", "on", "off"];
|
|
9
|
+
export function isAttachMode(value) {
|
|
10
|
+
return ATTACH_MODES.includes(value);
|
|
11
|
+
}
|
|
12
|
+
export function isAutoAttach(value) {
|
|
13
|
+
return AUTO_ATTACH_MODES.includes(value);
|
|
14
|
+
}
|
|
15
|
+
export function globalConfigPath() {
|
|
16
|
+
return join(storeDir(), "hive.yml");
|
|
17
|
+
}
|
|
18
|
+
function isMapping(value) {
|
|
19
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
20
|
+
return false;
|
|
21
|
+
const prototype = Object.getPrototypeOf(value);
|
|
22
|
+
return prototype === Object.prototype || prototype === null;
|
|
23
|
+
}
|
|
24
|
+
export function isEmptyYamlDocument(text) {
|
|
25
|
+
return text.split(/\r?\n/).every((line) => /^\s*(?:#.*)?$/.test(line));
|
|
26
|
+
}
|
|
27
|
+
function readLayer(path, kind, warnings) {
|
|
28
|
+
let text;
|
|
29
|
+
try {
|
|
30
|
+
text = readFileSync(path, "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== "ENOENT") {
|
|
34
|
+
warnings.push(`${path}: could not read configuration: ${errorMessage(error)}`);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
let value;
|
|
39
|
+
try {
|
|
40
|
+
value = kind === "yaml" ? parse(text) : JSON.parse(text);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
warnings.push(`${path}: ${kind === "yaml" ? "hive.yml is not valid YAML" : "config.json is not valid JSON"}: ${errorMessage(error)}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (kind === "yaml" && value == null && isEmptyYamlDocument(text))
|
|
47
|
+
return null;
|
|
48
|
+
if (!isMapping(value)) {
|
|
49
|
+
warnings.push(`${path}: ${kind === "yaml" ? "configuration must be a YAML mapping." : "config.json must be a JSON object."}`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
export function readGlobalConfig() {
|
|
55
|
+
const dir = storeDir();
|
|
56
|
+
const warnings = [];
|
|
57
|
+
const root = readLayer(join(dir, "hive.yml"), "yaml", warnings);
|
|
58
|
+
const legacy = readLayer(join(dir, "config.json"), "json", warnings) ?? {};
|
|
59
|
+
return { root, legacy, warnings };
|
|
60
|
+
}
|
|
61
|
+
function readEditableDocument(path, allowMissing) {
|
|
62
|
+
let text;
|
|
63
|
+
try {
|
|
64
|
+
text = readFileSync(path, "utf8");
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (allowMissing && error.code === "ENOENT")
|
|
68
|
+
return parseDocument("");
|
|
69
|
+
throw new Error(`${path}: could not read hive.yml: ${errorMessage(error)}`);
|
|
70
|
+
}
|
|
71
|
+
const document = parseDocument(text);
|
|
72
|
+
if (document.errors.length > 0) {
|
|
73
|
+
throw new Error(`${path}: hive.yml is not valid YAML: ${document.errors.map(errorMessage).join("; ")}`);
|
|
74
|
+
}
|
|
75
|
+
if (!isMap(document.contents))
|
|
76
|
+
throw new Error(`${path}: hive.yml must be a YAML mapping.`);
|
|
77
|
+
return document;
|
|
78
|
+
}
|
|
79
|
+
function writeAtomicYaml(path, text) {
|
|
80
|
+
let mode = 0o600;
|
|
81
|
+
try {
|
|
82
|
+
mode = statSync(path).mode & 0o777;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code !== "ENOENT")
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
const candidate = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
89
|
+
let temp = null;
|
|
90
|
+
let fd = null;
|
|
91
|
+
try {
|
|
92
|
+
fd = openSync(candidate, "wx", 0o600);
|
|
93
|
+
temp = candidate;
|
|
94
|
+
writeFileSync(fd, text, { encoding: "utf8" });
|
|
95
|
+
closeSync(fd);
|
|
96
|
+
fd = null;
|
|
97
|
+
chmodSync(candidate, mode);
|
|
98
|
+
renameSync(candidate, path);
|
|
99
|
+
temp = null;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (fd !== null) {
|
|
103
|
+
try {
|
|
104
|
+
closeSync(fd);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (temp !== null) {
|
|
110
|
+
try {
|
|
111
|
+
unlinkSync(temp);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code !== "ENOENT")
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function updateGlobalDocument(dir, update) {
|
|
121
|
+
mkdirSync(dir, { recursive: true });
|
|
122
|
+
const path = join(dir, "hive.yml");
|
|
123
|
+
const document = readEditableDocument(path, true);
|
|
124
|
+
if (!update(document))
|
|
125
|
+
return false;
|
|
126
|
+
let text;
|
|
127
|
+
try {
|
|
128
|
+
text = document.toString();
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw new Error(`${path}: could not serialize hive.yml: ${errorMessage(error)}`);
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
writeAtomicYaml(path, text);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
throw new Error(`${path}: could not write hive.yml: ${errorMessage(error)}`);
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
export function writeGlobalConfigKey(key, value) {
|
|
142
|
+
const dir = storeDir();
|
|
143
|
+
updateGlobalDocument(dir, (document) => {
|
|
144
|
+
document.set(key, value);
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
export function migrateLegacyConfig() {
|
|
149
|
+
const dir = storeDir();
|
|
150
|
+
const legacyPath = join(dir, "config.json");
|
|
151
|
+
const archivePath = `${legacyPath}.migrated`;
|
|
152
|
+
const globalPath = join(dir, "hive.yml");
|
|
153
|
+
const refusal = (path, warning) => ({
|
|
154
|
+
migrated: false,
|
|
155
|
+
notice: null,
|
|
156
|
+
warnings: [`${path}: ${warning}`],
|
|
157
|
+
});
|
|
158
|
+
let legacyText;
|
|
159
|
+
try {
|
|
160
|
+
legacyText = readFileSync(legacyPath, "utf8");
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error.code === "ENOENT")
|
|
164
|
+
return { migrated: false, notice: null, warnings: [] };
|
|
165
|
+
return refusal(legacyPath, `could not read config.json: ${errorMessage(error)}`);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
lstatSync(archivePath);
|
|
169
|
+
return refusal(legacyPath, `archive ${archivePath} already exists; refusing to overwrite it.`);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error.code !== "ENOENT") {
|
|
173
|
+
return refusal(archivePath, `could not check migration archive: ${errorMessage(error)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
let legacy;
|
|
177
|
+
try {
|
|
178
|
+
const raw = JSON.parse(legacyText);
|
|
179
|
+
if (!isMapping(raw))
|
|
180
|
+
return refusal(legacyPath, "config.json must be a JSON object.");
|
|
181
|
+
legacy = raw;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return refusal(legacyPath, `config.json is not valid JSON: ${errorMessage(error)}`);
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
updateGlobalDocument(dir, (document) => {
|
|
188
|
+
let changed = false;
|
|
189
|
+
for (const [key, value] of Object.entries(legacy)) {
|
|
190
|
+
if (key === "review_tags" || document.has(key))
|
|
191
|
+
continue;
|
|
192
|
+
document.set(key, value);
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
return changed;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const message = errorMessage(error);
|
|
200
|
+
const warning = message.startsWith(`${globalPath}:`)
|
|
201
|
+
? message
|
|
202
|
+
: `${globalPath}: could not update hive.yml: ${message}`;
|
|
203
|
+
return { migrated: false, notice: null, warnings: [warning] };
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
renameSync(legacyPath, archivePath);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
return refusal(legacyPath, `could not archive config.json as ${archivePath}: ${errorMessage(error)}`);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
migrated: true,
|
|
213
|
+
notice: `hive: migrated ${legacyPath} into ${globalPath}; original saved as ${archivePath}.`,
|
|
214
|
+
warnings: [],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function resolveAttachSetting(key, global, legacy, warnings) {
|
|
218
|
+
const mode = key === "attach";
|
|
219
|
+
const env = mode
|
|
220
|
+
? isAttachMode(process.env.HIVE_ATTACH_MODE) ? process.env.HIVE_ATTACH_MODE : null
|
|
221
|
+
: process.env.HIVE_AUTO_ATTACH === "0"
|
|
222
|
+
? "off"
|
|
223
|
+
: isAutoAttach(process.env.HIVE_AUTO_ATTACH) ? process.env.HIVE_AUTO_ATTACH : null;
|
|
224
|
+
if (env != null)
|
|
225
|
+
return mode ? { mode: env, source: "env" } : { value: env, source: "env" };
|
|
226
|
+
const path = globalConfigPath();
|
|
227
|
+
if (global && Object.hasOwn(global, key)) {
|
|
228
|
+
const value = global[key];
|
|
229
|
+
if (value == null)
|
|
230
|
+
return mode ? { mode: "auto", source: "global" } : { value: "auto", source: "global" };
|
|
231
|
+
if (mode ? isAttachMode(value) : isAutoAttach(value)) {
|
|
232
|
+
return mode ? { mode: value, source: "global" } : { value: value, source: "global" };
|
|
233
|
+
}
|
|
234
|
+
warnings.push(`${path}: ${key} must be one of ${(mode ? ["auto", "raw", "control"] : ["auto", "on", "off"]).join(", ")}; ignoring "${String(value)}".`);
|
|
235
|
+
return mode ? { mode: "auto", source: "built-in" } : { value: "auto", source: "built-in" };
|
|
236
|
+
}
|
|
237
|
+
if (Object.hasOwn(legacy, key)) {
|
|
238
|
+
const value = legacy[key];
|
|
239
|
+
if (value == null)
|
|
240
|
+
return mode ? { mode: "auto", source: "global" } : { value: "auto", source: "global" };
|
|
241
|
+
if (mode ? isAttachMode(value) : isAutoAttach(value)) {
|
|
242
|
+
return mode ? { mode: value, source: "global" } : { value: value, source: "global" };
|
|
243
|
+
}
|
|
244
|
+
warnings.push(`${path.replace(/hive\.yml$/, "config.json")}: ${key} has an invalid value; ignoring it.`);
|
|
245
|
+
}
|
|
246
|
+
return mode ? { mode: "auto", source: "built-in" } : { value: "auto", source: "built-in" };
|
|
247
|
+
}
|
package/dist/harnesses.js
CHANGED
|
@@ -171,3 +171,11 @@ export function transcriptDirFor(command) {
|
|
|
171
171
|
export function hasTranscriptSignal(row) {
|
|
172
172
|
return transcriptDirFor(row.command) || row.transcript_path !== "";
|
|
173
173
|
}
|
|
174
|
+
export function readOnlyExtraArgsAllowed(harness, args = []) {
|
|
175
|
+
const allowlist = {
|
|
176
|
+
codex: { flag: "-c", value: /^model_reasoning_effort=(low|medium|high|xhigh)$/ },
|
|
177
|
+
claude: { flag: "--effort", value: /^(low|medium|high|xhigh|max)$/ },
|
|
178
|
+
};
|
|
179
|
+
const allowed = allowlist[harness];
|
|
180
|
+
return allowed != null && args.length % 2 === 0 && args.every((arg, index) => index % 2 === 0 ? arg === allowed.flag : allowed.value.test(arg));
|
|
181
|
+
}
|
package/dist/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function helpOverview() {
|
|
2
|
-
return `HIVE —
|
|
2
|
+
return `HIVE — a lead session and a crew of Claude Code and Codex workers, sharing one local store
|
|
3
3
|
|
|
4
4
|
Many workers, one shared memory. Multiple Claude sessions (and humans) share
|
|
5
5
|
one project-scoped state store: pads, todos, kv, leases.
|
|
@@ -126,7 +126,7 @@ If the human asks to change how work runs here, that is an edit to the
|
|
|
126
126
|
runbook (fork it first), not a pad write.`,
|
|
127
127
|
agents: `AGENTS — spawn and drive worker sessions in tmux
|
|
128
128
|
|
|
129
|
-
agent_spawn(name?, model?, command?, extra_args?, cwd?, placement?, layout?) —
|
|
129
|
+
agent_spawn(name?, model?, command?, extra_args?, read_only?, cwd?, placement?, layout?) —
|
|
130
130
|
start a worker (default command: claude) in session hive-main.
|
|
131
131
|
placement="split" (default) tiles the worker as a pane in the lead's
|
|
132
132
|
window so the whole crew shares one screen; placement="window" gives it
|
package/dist/hooks.js
CHANGED
|
@@ -39,7 +39,8 @@ export function ensureWorkerHooksFile(agentId, options) {
|
|
|
39
39
|
const settings = {
|
|
40
40
|
...stateHookSettings(),
|
|
41
41
|
statusLine: statusLineEntry(effectiveClaudeStatusLine(worker.cwd)),
|
|
42
|
-
permissions: { deny: ["SendMessage", "ListAgents"] },
|
|
42
|
+
permissions: { deny: ["SendMessage", "ListAgents", ...(options.readOnly ? ["Edit", "Write", "NotebookEdit"] : [])] },
|
|
43
|
+
...(options.readOnly ? { sandbox: { enabled: true, failIfUnavailable: true, autoAllowBashIfSandboxed: true, allowUnsandboxedCommands: false, filesystem: { denyWrite: ["//"] } } } : {}),
|
|
43
44
|
crossSessionInbound: "refuse",
|
|
44
45
|
};
|
|
45
46
|
if (options.includePostToolUse)
|
package/dist/kickoff.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { cutToUnitBudget } from "./slug.js";
|
|
6
5
|
export const OUTPUT_BUDGET = 10_000;
|
|
@@ -116,12 +115,12 @@ export async function evaluate(cwd, opts = {}) {
|
|
|
116
115
|
catch {
|
|
117
116
|
return { fired: false, reason: "cwd does not exist" };
|
|
118
117
|
}
|
|
119
|
-
if (!existsSync(join(dir, "hive.yml")))
|
|
120
|
-
return { fired: false, reason: "no hive.yml here" };
|
|
121
118
|
const { activeProfile, DEFAULT_LEAD_BRANCHES, loadProjectYml } = await import("./projectYml.js");
|
|
122
119
|
const { profileExists } = await import("./profiles.js");
|
|
123
120
|
const { config, warnings } = loadProjectYml(dir);
|
|
124
121
|
const silent = (reason) => ({ fired: false, reason, warnings });
|
|
122
|
+
if (config == null)
|
|
123
|
+
return silent("no hive.yml here");
|
|
125
124
|
const profile = activeProfile(config);
|
|
126
125
|
if (!profile)
|
|
127
126
|
return silent("no profile in hive.yml");
|