@cmgmyr/hive 0.1.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/claude-plugin/.claude-plugin/plugin.json +7 -0
  4. package/claude-plugin/README.md +47 -0
  5. package/claude-plugin/hooks/hooks.json +14 -0
  6. package/claude-plugin/kickoff.mjs +41 -0
  7. package/claude-plugin/skills/cleanup/SKILL.md +60 -0
  8. package/dist/abi.js +220 -0
  9. package/dist/abiProbe.js +9 -0
  10. package/dist/backgroundTasks.js +51 -0
  11. package/dist/backup.js +378 -0
  12. package/dist/brief.js +113 -0
  13. package/dist/build-info.json +1 -0
  14. package/dist/claudeDir.js +5 -0
  15. package/dist/cli.js +2901 -0
  16. package/dist/codexHome.js +297 -0
  17. package/dist/config.js +72 -0
  18. package/dist/context.js +256 -0
  19. package/dist/contextCheckpoint.js +55 -0
  20. package/dist/dashboard.js +1158 -0
  21. package/dist/dataDir.js +89 -0
  22. package/dist/db.js +421 -0
  23. package/dist/dispatcher.js +150 -0
  24. package/dist/firstPrompt.js +6 -0
  25. package/dist/harnesses.js +173 -0
  26. package/dist/help.js +353 -0
  27. package/dist/hook.js +197 -0
  28. package/dist/hooks.js +50 -0
  29. package/dist/index.js +34 -0
  30. package/dist/kickoff.js +191 -0
  31. package/dist/leadMessage.js +98 -0
  32. package/dist/mcpConfig.js +121 -0
  33. package/dist/processes.js +161 -0
  34. package/dist/profiles.js +278 -0
  35. package/dist/projectYml.js +254 -0
  36. package/dist/prompts.js +18 -0
  37. package/dist/ptys.js +146 -0
  38. package/dist/result.js +53 -0
  39. package/dist/scheduler.js +1622 -0
  40. package/dist/sessionProbe.js +131 -0
  41. package/dist/slug.js +29 -0
  42. package/dist/spawn.js +370 -0
  43. package/dist/stateProvenance.js +97 -0
  44. package/dist/statusline.js +82 -0
  45. package/dist/strictInput.js +52 -0
  46. package/dist/teardown.js +89 -0
  47. package/dist/tmux.js +1295 -0
  48. package/dist/tools/agents.js +1246 -0
  49. package/dist/tools/kv.js +89 -0
  50. package/dist/tools/leases.js +87 -0
  51. package/dist/tools/meta.js +183 -0
  52. package/dist/tools/pads.js +279 -0
  53. package/dist/tools/params.js +14 -0
  54. package/dist/tools/todos.js +382 -0
  55. package/dist/tools/wakes.js +466 -0
  56. package/dist/transcript.js +123 -0
  57. package/dist/version.js +59 -0
  58. package/package.json +62 -0
  59. package/profiles/orchestration/posture.md +27 -0
  60. package/profiles/orchestration/runbook.md +72 -0
  61. package/profiles/orchestration/worker.md +38 -0
  62. package/profiles/simple/posture.md +14 -0
@@ -0,0 +1,254 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
3
+ import { join, resolve, sep } from "node:path";
4
+ import { parse } from "yaml";
5
+ import { harnessNames } from "./harnesses.js";
6
+ import { isValidProfileName } from "./profiles.js";
7
+ import { errorMessage } from "./result.js";
8
+ import { isWindowLayout, WINDOW_LAYOUTS } from "./tmux.js";
9
+ export const DEFAULT_LEAD_BRANCHES = ["main", "master"];
10
+ export const NO_PROFILE = "none";
11
+ export function activeProfile(config) {
12
+ const name = config?.profile;
13
+ return name == null || name === NO_PROFILE ? null : name;
14
+ }
15
+ // First entry is the default; absent or empty means claude only (todo 526).
16
+ export function allowedAgents(config) {
17
+ const list = config?.agents;
18
+ return list && list.length > 0 ? list : ["claude"];
19
+ }
20
+ function agentVarKey(harness) {
21
+ return `agents_${harness}`;
22
+ }
23
+ // One var per allowed harness, "1" - distinct from brief.ts's per-spawn harness_claude/harness_codex.
24
+ export function agentVars(config) {
25
+ return Object.fromEntries(allowedAgents(config).map((name) => [agentVarKey(name), "1"]));
26
+ }
27
+ // Every known harness's key, not just currently-allowed ones - a strip for a disallowed harness
28
+ // is correct, not missing, so doctor's scan must exclude the whole family.
29
+ export function agentVarKeys() {
30
+ return harnessNames().map(agentVarKey);
31
+ }
32
+ // Mirrors mergedBriefVars (src/brief.ts): strip any project-defined agents_* var first so a lying
33
+ // one can never silently win, then spread the derived vars on top.
34
+ export function mergedProjectVars(config) {
35
+ const safe = { ...(config?.vars ?? {}) };
36
+ for (const key of agentVarKeys())
37
+ delete safe[key];
38
+ return { ...safe, ...agentVars(config) };
39
+ }
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;
46
+ 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 };
52
+ }
53
+ if (raw == null || typeof raw !== "object") {
54
+ warnings.push("hive.yml must be a YAML mapping.");
55
+ return { config: null, warnings };
56
+ }
57
+ const root = raw;
58
+ let context_checkpoint_percent = null;
59
+ if (root.context_checkpoint_percent != null) {
60
+ const value = root.context_checkpoint_percent;
61
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 100) {
62
+ context_checkpoint_percent = value;
63
+ }
64
+ else {
65
+ warnings.push("context_checkpoint_percent must be an integer from 1 through 100; ignoring it.");
66
+ }
67
+ }
68
+ const lead = typeof root.lead === "string" && root.lead.trim() !== "" ? root.lead.trim() : null;
69
+ let placement = null;
70
+ if (root.placement != null) {
71
+ if (root.placement === "split" || root.placement === "window") {
72
+ placement = root.placement;
73
+ }
74
+ else {
75
+ warnings.push(`placement must be "split" or "window"; ignoring "${String(root.placement)}".`);
76
+ }
77
+ }
78
+ let layout = null;
79
+ if (root.layout != null) {
80
+ if (isWindowLayout(root.layout)) {
81
+ layout = root.layout;
82
+ }
83
+ else {
84
+ warnings.push(`layout must be one of ${WINDOW_LAYOUTS.join(", ")}; ignoring "${String(root.layout)}".`);
85
+ }
86
+ }
87
+ let profile = null;
88
+ if (root.profile != null) {
89
+ const value = String(root.profile).trim();
90
+ if (value === "") {
91
+ warnings.push("profile is empty; ignoring it.");
92
+ }
93
+ else if (value !== NO_PROFILE && !isValidProfileName(value)) {
94
+ warnings.push(`profile "${value}" is not a valid profile name (letters, digits, dot, dash, underscore); ignoring it.`);
95
+ }
96
+ else {
97
+ profile = value;
98
+ }
99
+ }
100
+ let agents = null;
101
+ if (root.agents != null) {
102
+ if (!Array.isArray(root.agents)) {
103
+ warnings.push("agents must be a list of harness names; ignoring it.");
104
+ }
105
+ else {
106
+ const known = harnessNames();
107
+ const valid = [];
108
+ for (const entry of root.agents) {
109
+ const value = typeof entry === "string" ? entry.trim() : "";
110
+ if (value === "") {
111
+ warnings.push("agents entry must be a non-empty harness name; skipped.");
112
+ }
113
+ else if (!known.includes(value)) {
114
+ warnings.push(`agents: "${value}" is not a known harness (${known.join(", ")}); ignoring it.`);
115
+ }
116
+ else {
117
+ valid.push(value);
118
+ }
119
+ }
120
+ agents = valid.length > 0 ? valid : null;
121
+ }
122
+ }
123
+ const review_tags = [];
124
+ if (root.review_tags != null) {
125
+ if (!Array.isArray(root.review_tags)) {
126
+ warnings.push("review_tags must be a list of todo tags; ignoring it.");
127
+ }
128
+ else {
129
+ for (const entry of root.review_tags) {
130
+ const value = typeof entry === "string" ? entry.trim() : "";
131
+ if (value === "") {
132
+ warnings.push("review_tags entry must be a non-empty tag; skipped.");
133
+ }
134
+ else if (!review_tags.includes(value)) {
135
+ review_tags.push(value);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ let lead_branches = null;
141
+ if (root.lead_branches != null) {
142
+ const raw = Array.isArray(root.lead_branches) ? root.lead_branches : null;
143
+ const branches = raw
144
+ ?.filter((b) => typeof b === "string" && b.trim() !== "")
145
+ .map((b) => b.trim());
146
+ if (branches && branches.length === raw.length) {
147
+ lead_branches = branches;
148
+ }
149
+ else {
150
+ warnings.push("lead_branches must be a list of branch names; ignoring it.");
151
+ }
152
+ }
153
+ let dashboard = false;
154
+ if (root.dashboard != null) {
155
+ if (typeof root.dashboard === "boolean") {
156
+ dashboard = root.dashboard;
157
+ }
158
+ else {
159
+ warnings.push(`dashboard must be true or false; ignoring "${String(root.dashboard)}".`);
160
+ }
161
+ }
162
+ const vars = {};
163
+ if (root.vars != null) {
164
+ if (typeof root.vars !== "object" || Array.isArray(root.vars)) {
165
+ warnings.push("vars must be a mapping of name to value; ignoring it.");
166
+ }
167
+ else {
168
+ for (const [key, value] of Object.entries(root.vars)) {
169
+ if (value == null || typeof value === "object") {
170
+ warnings.push(`var "${key}" must be a scalar; skipped.`);
171
+ continue;
172
+ }
173
+ vars[key] = String(value);
174
+ }
175
+ }
176
+ }
177
+ const processes = {};
178
+ const rawProcesses = root.processes;
179
+ if (rawProcesses != null) {
180
+ if (typeof rawProcesses !== "object") {
181
+ warnings.push("hive.yml `processes` must be a mapping of name to command.");
182
+ }
183
+ else {
184
+ for (const [name, value] of Object.entries(rawProcesses)) {
185
+ if (typeof value === "string" && value.trim() !== "") {
186
+ processes[name] = { command: value.trim(), dir: null, auto_start: true, visible: true, env: {} };
187
+ continue;
188
+ }
189
+ if (value == null || typeof value !== "object") {
190
+ warnings.push(`Process "${name}" needs a command string; skipped.`);
191
+ continue;
192
+ }
193
+ const p = value;
194
+ if (typeof p.command !== "string" || p.command.trim() === "") {
195
+ warnings.push(`Process "${name}" has no command; skipped.`);
196
+ continue;
197
+ }
198
+ const dirValue = p.dir ?? p.working_dir;
199
+ let visible = true;
200
+ if (p.visible != null) {
201
+ if (typeof p.visible === "boolean") {
202
+ visible = p.visible;
203
+ }
204
+ else {
205
+ warnings.push(`Process "${name}": visible must be true or false; ignoring "${String(p.visible)}".`);
206
+ }
207
+ }
208
+ const env = {};
209
+ if (p.env != null && typeof p.env === "object") {
210
+ for (const [k, v] of Object.entries(p.env)) {
211
+ env[k] = String(v);
212
+ }
213
+ }
214
+ processes[name] = {
215
+ command: p.command.trim(),
216
+ dir: typeof dirValue === "string" && dirValue.trim() !== "" ? dirValue.trim() : null,
217
+ auto_start: p.auto_start !== false,
218
+ visible,
219
+ env,
220
+ };
221
+ }
222
+ }
223
+ }
224
+ return {
225
+ config: {
226
+ lead,
227
+ placement,
228
+ layout,
229
+ profile,
230
+ agents,
231
+ review_tags,
232
+ lead_branches,
233
+ context_checkpoint_percent,
234
+ dashboard,
235
+ vars,
236
+ processes,
237
+ },
238
+ warnings,
239
+ };
240
+ }
241
+ export function configHash(name, command, dir, env) {
242
+ return createHash("sha256")
243
+ .update(JSON.stringify([name, command, dir, env]))
244
+ .digest("hex");
245
+ }
246
+ export function resolveCommandDir(projectPath, dir) {
247
+ if (dir == null)
248
+ return projectPath;
249
+ const resolved = realpathSync(resolve(projectPath, dir));
250
+ if (resolved !== projectPath && !resolved.startsWith(projectPath + sep)) {
251
+ throw new Error(`dir "${dir}" escapes the project root`);
252
+ }
253
+ return resolved;
254
+ }
@@ -0,0 +1,18 @@
1
+ const RUNBOOK_PROMPT = {
2
+ name: "runbook",
3
+ description: "Load this project's own standing process before planning or dispatching anything.",
4
+ text: `Run \`hive runbook\` in a shell and follow what it prints. It resolves this
5
+ project's profile runbook, or its runbook pad when the project has no
6
+ profile, with hive.yml vars substituted.
7
+
8
+ If the project has neither, run help(topic="workflow") for hive's generic
9
+ lead/worker pattern and agree a process with the human before dispatching
10
+ anything.`,
11
+ };
12
+ export function registerPrompts(server) {
13
+ server.registerPrompt(RUNBOOK_PROMPT.name, { description: RUNBOOK_PROMPT.description }, () => ({
14
+ messages: [
15
+ { role: "user", content: { type: "text", text: RUNBOOK_PROMPT.text } },
16
+ ],
17
+ }));
18
+ }
package/dist/ptys.js ADDED
@@ -0,0 +1,146 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { platform } from "node:os";
4
+ const PS_LINE = /^\s*(\S+)\s+(\d+)\s+(.+?)\s*$/;
5
+ export function parsePsSnapshot(output) {
6
+ const rows = [];
7
+ for (const line of output.split("\n")) {
8
+ if (line.trim() === "")
9
+ continue;
10
+ const m = PS_LINE.exec(line);
11
+ if (!m)
12
+ continue;
13
+ rows.push({ tty: m[1], ppid: Number(m[2]), comm: m[3] });
14
+ }
15
+ return rows;
16
+ }
17
+ export function countAllocatedTtys(rows) {
18
+ return new Set(rows.filter((r) => /^ttys/.test(r.tty)).map((r) => r.tty)).size;
19
+ }
20
+ export function isOrphanLoginShell(row) {
21
+ if (row.ppid !== 1)
22
+ return false;
23
+ const arg0 = row.comm.split(/\s+/, 1)[0] ?? row.comm;
24
+ const base = arg0.split("/").pop() ?? arg0;
25
+ return base.startsWith("-");
26
+ }
27
+ export function countOrphanLoginShells(rows) {
28
+ return rows.filter(isOrphanLoginShell).length;
29
+ }
30
+ function psSnapshot(field) {
31
+ return parsePsSnapshot(execFileSync("ps", ["-eo", `tty=,ppid=,${field}=`], {
32
+ encoding: "utf8",
33
+ stdio: ["ignore", "pipe", "ignore"],
34
+ }));
35
+ }
36
+ export function ptyWarnThreshold(max) {
37
+ return Math.max(32, Math.floor(max / 10));
38
+ }
39
+ export function isLowHeadroom(headroom) {
40
+ const free = headroom.max - headroom.allocated;
41
+ return free < ptyWarnThreshold(headroom.max);
42
+ }
43
+ function isRealCount(n) {
44
+ return Number.isFinite(n) && n >= 0;
45
+ }
46
+ const SYSCTL = "/usr/sbin/sysctl";
47
+ function darwinHeadroom() {
48
+ const max = Number(execFileSync(SYSCTL, ["-n", "kern.tty.ptmx_max"], {
49
+ encoding: "utf8",
50
+ stdio: ["ignore", "pipe", "ignore"],
51
+ }).trim());
52
+ if (!isRealCount(max) || max === 0)
53
+ throw new Error("sysctl kern.tty.ptmx_max did not report a usable number");
54
+ const rows = psSnapshot("comm");
55
+ return {
56
+ allocated: countAllocatedTtys(rows),
57
+ max,
58
+ orphanLoginShells: countOrphanLoginShells(rows),
59
+ source: `${SYSCTL} -n kern.tty.ptmx_max, ps -eo tty=,ppid=,comm=`,
60
+ };
61
+ }
62
+ function linuxHeadroom() {
63
+ const max = Number(readFileSync("/proc/sys/kernel/pty/max", "utf8").trim());
64
+ const allocated = Number(readFileSync("/proc/sys/kernel/pty/nr", "utf8").trim());
65
+ if (!isRealCount(max) || max === 0 || !isRealCount(allocated)) {
66
+ throw new Error("/proc/sys/kernel/pty/{max,nr} did not report usable numbers");
67
+ }
68
+ let orphanLoginShells = 0;
69
+ try {
70
+ orphanLoginShells = countOrphanLoginShells(psSnapshot("args"));
71
+ }
72
+ catch {
73
+ }
74
+ return { allocated, max, orphanLoginShells, source: "/proc/sys/kernel/pty/{max,nr}" };
75
+ }
76
+ export function probeForPlatform(osPlatform) {
77
+ if (osPlatform === "darwin")
78
+ return darwinHeadroom;
79
+ if (osPlatform === "linux")
80
+ return linuxHeadroom;
81
+ return null;
82
+ }
83
+ export function safeProbe(probe) {
84
+ try {
85
+ return probe();
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
91
+ export function ptyHeadroom() {
92
+ try {
93
+ if (process.env.HIVE_PTY_HEADROOM_JSON)
94
+ return JSON.parse(process.env.HIVE_PTY_HEADROOM_JSON);
95
+ const probe = probeForPlatform(platform());
96
+ return probe ? safeProbe(probe) : null;
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ const PS_AGE_LINE = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.+?)\s*$/;
103
+ export function parseEtimeSeconds(etime) {
104
+ const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime) ?? /^(\d+)$/.exec(etime);
105
+ if (!m)
106
+ return null;
107
+ if (m.length === 2)
108
+ return Number(m[1]);
109
+ const [, days, hours, minutes, seconds] = m;
110
+ return (Number(days ?? 0) * 24 + Number(hours ?? 0)) * 3600 + Number(minutes) * 60 + Number(seconds);
111
+ }
112
+ export function parsePsSnapshotWithAge(output) {
113
+ const rows = [];
114
+ for (const line of output.split("\n")) {
115
+ if (line.trim() === "")
116
+ continue;
117
+ const m = PS_AGE_LINE.exec(line);
118
+ if (!m)
119
+ continue;
120
+ const seconds = parseEtimeSeconds(m[3]);
121
+ if (seconds === null)
122
+ continue;
123
+ rows.push({ pid: Number(m[1]), ppid: Number(m[2]), ageMs: seconds * 1000, tty: m[4], comm: m[5] });
124
+ }
125
+ return rows;
126
+ }
127
+ export function filterOrphanLoginShellsWithAge(rows) {
128
+ return rows.filter(isOrphanLoginShell);
129
+ }
130
+ function psSnapshotWithAge() {
131
+ try {
132
+ if (process.env.HIVE_PTY_PS_ROWS_JSON)
133
+ return JSON.parse(process.env.HIVE_PTY_PS_ROWS_JSON);
134
+ return parsePsSnapshotWithAge(execFileSync("ps", ["-eo", "pid=,ppid=,etime=,tty=,comm="], {
135
+ encoding: "utf8",
136
+ stdio: ["ignore", "pipe", "ignore"],
137
+ }));
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ export function orphanLoginShellDetails() {
144
+ const rows = psSnapshotWithAge();
145
+ return rows === null ? null : filterOrphanLoginShellsWithAge(rows);
146
+ }
package/dist/result.js ADDED
@@ -0,0 +1,53 @@
1
+ export function errorMessage(e) {
2
+ return e instanceof Error ? e.message : String(e);
3
+ }
4
+ function ok(data) {
5
+ const text = typeof data === "string" ? data : JSON.stringify(data, null, 1);
6
+ return { content: [{ type: "text", text }] };
7
+ }
8
+ const STORE_REPLACED_MESSAGE = "hive: the store on disk was replaced out from under this session (likely a restore). " +
9
+ "This session has been writing to a file that no longer exists on disk; up to one tick " +
10
+ "of writes may already be lost. Restart this session before doing anything else.";
11
+ export function registrationNoticeText(notice) {
12
+ return (`hive: no registered project matched this session's working directory, so it created ` +
13
+ `project ${notice.id} at "${notice.path}". If that's unintended: project_prune removes ` +
14
+ `it if it owns nothing yet, run the session from the intended directory, or call ` +
15
+ `project_select.`);
16
+ }
17
+ export async function run(fn) {
18
+ const { storeReplaced } = await import("./db.js");
19
+ if (storeReplaced()) {
20
+ return { content: [{ type: "text", text: STORE_REPLACED_MESSAGE }], isError: true };
21
+ }
22
+ const { takeRegistrationNotice } = await import("./context.js");
23
+ let result;
24
+ try {
25
+ result = ok(await fn());
26
+ }
27
+ catch (e) {
28
+ result = { content: [{ type: "text", text: errorMessage(e) }], isError: true };
29
+ }
30
+ const notice = takeRegistrationNotice();
31
+ if (notice) {
32
+ result.content.push({ type: "text", text: registrationNoticeText(notice) });
33
+ }
34
+ return result;
35
+ }
36
+ export function parseTags(text) {
37
+ try {
38
+ const parsed = JSON.parse(text);
39
+ return Array.isArray(parsed) ? parsed : [];
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ export function matchesAnyTag(rowTags, wanted) {
46
+ if (!wanted || wanted.length === 0)
47
+ return true;
48
+ const tags = parseTags(rowTags);
49
+ return wanted.some((t) => tags.includes(t));
50
+ }
51
+ export function withTrailingNewline(text) {
52
+ return text === "" || text.endsWith("\n") ? text : `${text}\n`;
53
+ }