@cmgmyr/hive 1.11.7 → 1.11.9

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 CHANGED
@@ -57,7 +57,7 @@ hive is a personal daily-driver tool, released low-key. It is single-user by des
57
57
 
58
58
  | Page | What's there |
59
59
  |---|---|
60
- | [Architecture](docs/architecture.md) | Seven diagrams: process topology, module layering, spawn sequence, wake lifecycle, worker state, project scoping, store and server identity |
60
+ | [Architecture](docs/architecture.md) | Eight diagrams: process topology, module layering, spawn sequence, wake lifecycle, worker state (with the lead handoff), project scoping, store and server identity |
61
61
  | [Patterns](docs/patterns.md) | Standing trades, refused approaches, evidence standards, and guard shapes distilled from the project's own decisions and dead-ends |
62
62
  | [Concepts](docs/concepts.md) | Vocabulary, why not subagents, identity, the workflow, project scope, the shared store |
63
63
  | [Daily driver](docs/daily-driver.md) | A day with hive, starting a session, watching workers, wake-ups |
@@ -51,4 +51,4 @@ path is ever written into a committed file.
51
51
 
52
52
  ## The crew mod
53
53
 
54
- `crew/` is a second, separate plugin (`hive-crew`) that `hive lead` loads with `--plugin-dir` only when `lead_sidebar: true` is set and the lead is Claude. It is not part of the symlink install above and workers never load it. See [docs/crew.md](../docs/crew.md).
54
+ `crew/` is a second, separate plugin (`hive-crew`) that `hive lead` loads with `--plugin-dir` for Claude leads by default. Set `lead_sidebar: false` to disable it. It is not part of the symlink install above and workers never load it. See [docs/crew.md](../docs/crew.md).
@@ -1 +1 @@
1
- {"version":"1.11.7","sha":"4861c81","dirty":false,"build_id":"2b159fa2c2519fbd7edef9cf4396ac349c3fb25ec433f76a9b91c5ad98188f43"}
1
+ {"version":"1.11.9","sha":"5d08b61","dirty":false,"build_id":"c6006fe410e3627175581a5b66e7d8fbb68b92f1b2edc49a00ce1c9256bacc30"}
@@ -1,7 +1,9 @@
1
1
  import { createConnection } from "node:net";
2
2
  import { join } from "node:path";
3
- import { db } from "./db.js";
4
- import { loadProjectYml } from "./projectYml.js";
3
+ import { dataDir, db } from "./db.js";
4
+ import { harnessFor } from "./harnesses.js";
5
+ import { loadProjectYml, quietMessagingEnabled } from "./projectYml.js";
6
+ import { shellQuote } from "./tmux.js";
5
7
  export { DELIVERY_METHOD } from "./wakeSql.js";
6
8
  export const CROSS_SESSION_CLOSE = "</cross-session-message>";
7
9
  export const ENVELOPE_OPEN = '<cross-session-message from="';
@@ -23,7 +25,12 @@ function claudeWakeFrame(sender, text) {
23
25
  }
24
26
  export function quietMessagingOn(projectId) {
25
27
  const project = db.prepare("SELECT path FROM projects WHERE id = ?").get(projectId);
26
- return project !== undefined && loadProjectYml(project.path).config?.quiet_messaging === true;
28
+ return project !== undefined && quietMessagingEnabled(loadProjectYml(project.path).config);
29
+ }
30
+ export function quietMessagingHooksLaunched(projectId, command) {
31
+ const path = join(dataDir, `lead-${projectId}-hooks.json`);
32
+ const args = harnessFor("claude").briefDelivery?.settingsArgs(path);
33
+ return args !== undefined && command.includes(args.map(shellQuote).join(" "));
27
34
  }
28
35
  export function promptMarkerEvidenceSql(markerAt) {
29
36
  const evPrompt = "(CASE WHEN json_valid(ev.payload) THEN COALESCE(json_extract(ev.payload, '$.prompt'), '') ELSE ev.payload END)";
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ import { adoptableWindow, applyLayout, claimInitialWindow, configureHiveWindow,
38
38
  import { collectCrew, crewStoreProblem } from "./crew.js";
39
39
  import { describeVisibility, formatLocal, processCounts } from "./dashboard.js";
40
40
  import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, renderStart, STOP_REASONS, } from "./processes.js";
41
- import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
41
+ import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, leadSidebarEnabled, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, quietMessagingEnabled, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
42
42
  import { appendClaudeLeadArgs, crewPluginDir, isTrusted, leadIdentityEnv, renderLeadPosture } from "./leadLaunch.js";
43
43
  import { handoffStatusSegment } from "./leadHandoff.js";
44
44
  import { cmdLeadHandoff, runHandoffGrace, runHandoffStart } from "./leadHandoffCli.js";
@@ -421,7 +421,7 @@ async function cmdLead(argv) {
421
421
  for (const w of warnings)
422
422
  console.log(`! ${w}`);
423
423
  const session = sessionName();
424
- const hooksPath = ensureLeadHooksFile(project.id, config?.quiet_messaging === true);
424
+ const hooksPath = ensureLeadHooksFile(project.id, quietMessagingEnabled(config));
425
425
  let leadCommand = "claude";
426
426
  if (!config) {
427
427
  console.log("No hive.yml here; starting a plain claude lead. Add a hive.yml to define project commands and a custom lead (run hive --help for the format).");
@@ -464,7 +464,7 @@ async function cmdLead(argv) {
464
464
  projectId: project.id,
465
465
  projectName: project.name,
466
466
  hooksPath,
467
- sidebarPluginDir: config?.lead_sidebar === true && leadHarness.name === "claude" ? crewPluginDir() : null,
467
+ sidebarPluginDir: leadSidebarEnabled(config) && leadHarness.name === "claude" ? crewPluginDir() : null,
468
468
  posture: renderedPosture,
469
469
  firstMessage,
470
470
  }));
@@ -749,13 +749,13 @@ const HIVE_YML_TEMPLATE = `# hive project config. Read by \`hive lead\` from the
749
749
 
750
750
  # context_checkpoint_percent: null # unset means off; integer 1-100 to enable
751
751
 
752
- # lead_turn_budget: {warn: 300, stop: 600} # optional lead statusline thresholds
752
+ # lead_turn_budget: {warn: 300, stop: 600} # optional thresholds; auto_handoff defaults on
753
753
 
754
754
  # dashboard: true # write .hive/dashboard.html (default: false)
755
755
 
756
- # lead_sidebar: true # a crew pane in a claude lead (default: false; restart the lead)
756
+ # lead_sidebar: false # disable the crew pane in a claude lead; restart the lead
757
757
 
758
- # quiet_messaging: true # wakes and worker messages reach a claude lead as one row (default: false; restart the lead)
758
+ # quiet_messaging: false # use typed wakes and worker messages instead of one row; restart the lead
759
759
 
760
760
  # vars: # substituted into the profile runbook
761
761
  # repo: owner/name # {{repo}}
package/dist/help.js CHANGED
@@ -253,7 +253,9 @@ Delivery contract:
253
253
  and the next action. Plain English, no markup.
254
254
  Waking your OWN lead pane is the exception: it is not cold, so carry the
255
255
  action and the ids and point at where the detail lives.
256
- With lead_turn_budget.auto_handoff on, a lead past its warn budget gets
256
+ With a lead_turn_budget, auto_handoff is on by default. Set
257
+ lead_turn_budget.auto_handoff to false to keep the lead in this session.
258
+ When enabled, a lead past its warn budget gets
257
259
  a "hive lead handoff request #N" wake: follow its steps, then run
258
260
  hive lead-handoff. A human prompt during the grace postpones it.
259
261
 
package/dist/index.js CHANGED
@@ -15,11 +15,12 @@ import { registerQueenAudit } from "./tools/queenAudit.js";
15
15
  import { registerPrompts } from "./prompts.js";
16
16
  import { schedulerIntervalMs, startScheduler } from "./scheduler.js";
17
17
  import { enforceStrictInput } from "./strictInput.js";
18
+ import { hiveVersion } from "./version.js";
18
19
  migrate();
19
20
  const SCOPE_DISCIPLINE = isQueenLead()
20
21
  ? "Scope discipline: you are the queen, the lead of hive's queen home. Your own project is the working directory's. You may read any registered project by passing its project_id. Into another project you may write only todo_create and todo_comment, plus wakes and agent_send text addressed to that project's running lead; every other write there refuses with QUEEN_CROSS_PROJECT_WRITE_REFUSED. See help(topic=\"projects\")."
21
22
  : "Scope discipline: all state is project-scoped, and the current project is the working directory's. An empty result means there is nothing IN THIS PROJECT; report that and stop. Never browse other projects' state (project_id overrides, project_select) unless the user explicitly names another project and asks for it.";
22
- const server = enforceStrictInput(new McpServer({ name: "hive", version: "0.1.0" }, {
23
+ const server = enforceStrictInput(new McpServer({ name: "hive", version: hiveVersion() }, {
23
24
  instructions: `Hive gives Claude Code sessions shared, persistent state for coordinating work on a project: pads for plans and findings, todos for tracked work with blockers, kv for small shared values, leases for claiming shared state, agents for spawning and directing other sessions, and wake-ups for scheduled or idle-triggered check-ins. Search for these tools when a task needs to persist past this conversation, hand off to or watch another session, or track multi-step work with dependencies.
24
25
  Getting started:
25
26
  1. Call whoami to see your actor identity and effective project scope.
@@ -11,8 +11,11 @@ import { readTurnCount } from "./turnCount.js";
11
11
  export { casHandoff, HELD_REASON_HANDOFF, handoffHoldsWake, readActiveHandoff, readHandoff, readHandoffGate, SUCCESSOR_NOT_READY, } from "./leadHandoffState.js";
12
12
  export const HANDOFF_PAD_NAME = "hive-lead-handoff";
13
13
  export const sha256 = (text) => createHash("sha256").update(text).digest("hex");
14
+ function autoHandoffEnabled(budget) {
15
+ return budget !== null && (budget.auto_handoff ?? true);
16
+ }
14
17
  export function handoffEligibility(lead, budget, snapshot = readLeadSafetySnapshot(lead.id)) {
15
- if (budget === null || budget.auto_handoff !== true)
18
+ if (budget === null || !autoHandoffEnabled(budget))
16
19
  return { eligible: false, why: "auto_handoff is not enabled" };
17
20
  if (harnessFor(lead.command).name !== "claude")
18
21
  return { eligible: false, why: "the lead is not a Claude lead" };
@@ -274,7 +277,7 @@ export function driveLeadHandoff(actorId, budgetFor) {
274
277
  settleAfterRespawn(row, lead);
275
278
  return;
276
279
  }
277
- if (budget?.auto_handoff !== true) {
280
+ if (!autoHandoffEnabled(budget)) {
278
281
  failHandoff(row.id, "lead_turn_budget.auto_handoff is no longer on");
279
282
  return;
280
283
  }
@@ -364,7 +367,7 @@ export function padProblem(pad, projectId, revision) {
364
367
  }
365
368
  // The final quiet check. A screen or probe that cannot be read waits; it never establishes quiet.
366
369
  export async function respawnVerdict(row, lead, budget) {
367
- if (budget?.auto_handoff !== true)
370
+ if (!autoHandoffEnabled(budget))
368
371
  return { fail: "lead_turn_budget.auto_handoff is no longer on" };
369
372
  if (lead === undefined || lead.pane_pid !== row.predecessor_pane_pid || lead.tmux_target !== row.pane_target) {
370
373
  return { fail: "the lead row no longer names the pane this handoff was requested for" };
@@ -7,7 +7,7 @@ import { cliPath } from "./dispatcher.js";
7
7
  import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
8
8
  import { ensureLeadHooksFile } from "./hooks.js";
9
9
  import { appendClaudeLeadArgs, crewPluginDir, isTrusted, leadIdentityEnv, renderLeadPosture } from "./leadLaunch.js";
10
- import { loadProjectYml } from "./projectYml.js";
10
+ import { leadSidebarEnabled, loadProjectYml, quietMessagingEnabled } from "./projectYml.js";
11
11
  import { buildEnvFlags } from "./spawn.js";
12
12
  import { panePidForRecord, rowOwnership, shellQuote, tmux, TmuxTimeoutError } from "./tmux.js";
13
13
  import { automationBlocker, failHandoff, missingHeadings, padProblem, readPad, RESPAWN_WAIT_LIMIT_SECONDS, respawnVerdict, runningLeadByActor, sha256, successorPrompt, } from "./leadHandoff.js";
@@ -152,8 +152,8 @@ async function successorLaunch(row, lead, attempt, token) {
152
152
  harness,
153
153
  projectId: lead.project_id,
154
154
  projectName: lead.name,
155
- hooksPath: ensureLeadHooksFile(lead.project_id, config?.quiet_messaging === true),
156
- sidebarPluginDir: config?.lead_sidebar === true ? crewPluginDir() : null,
155
+ hooksPath: ensureLeadHooksFile(lead.project_id, quietMessagingEnabled(config)),
156
+ sidebarPluginDir: leadSidebarEnabled(config) ? crewPluginDir() : null,
157
157
  posture: renderLeadPosture(config).rendered,
158
158
  firstMessage,
159
159
  });
@@ -1,5 +1,5 @@
1
1
  import { dataDir, db } from "./db.js";
2
- import { CROSS_SESSION_CLOSE, promptMarkerEvidenceSql, postClaudeWake, quietMessagingOn, socketGraceDue, socketSenderAddress, SOCKET_MESSAGE_FOOTER, } from "./claudeWake.js";
2
+ import { CROSS_SESSION_CLOSE, promptMarkerEvidenceSql, quietMessagingHooksLaunched, postClaudeWake, quietMessagingOn, socketGraceDue, socketSenderAddress, SOCKET_MESSAGE_FOOTER, } from "./claudeWake.js";
3
3
  import { harnessFor, paneClassifierFor } from "./harnesses.js";
4
4
  import { LEAD_MESSAGE_THRESHOLD, MESSAGE_METHOD, MESSAGE_RETENTION, MESSAGE_STATUS, renderLeadMessage, senderTag, shortenedSendNote, storeLeadMessage, } from "./leadMessage.js";
5
5
  import { readHandoffGate } from "./leadHandoffState.js";
@@ -30,7 +30,7 @@ function eligible(options) {
30
30
  const sender = db
31
31
  .prepare("SELECT 1 AS hit FROM agents WHERE project_id = ? AND actor_id = ? AND status = 'running' AND kind = 'agent'")
32
32
  .get(projectId, fromActor);
33
- return sender !== undefined && quietMessagingOn(projectId);
33
+ return sender !== undefined && quietMessagingOn(projectId) && quietMessagingHooksLaunched(projectId, target.command);
34
34
  }
35
35
  function registeredSocket(target) {
36
36
  return (target.claude_messaging_socket !== "" &&
@@ -18,8 +18,8 @@ export const BUILT_IN_PROJECT_YML = Object.freeze({
18
18
  context_checkpoint_percent: null,
19
19
  lead_turn_budget: null,
20
20
  dashboard: false,
21
- lead_sidebar: false,
22
- quiet_messaging: false,
21
+ lead_sidebar: true,
22
+ quiet_messaging: true,
23
23
  vars: Object.freeze({}),
24
24
  processes: Object.freeze({}),
25
25
  });
@@ -33,6 +33,12 @@ export function activeProfile(config) {
33
33
  const name = config?.profile;
34
34
  return name == null || name === NO_PROFILE ? null : name;
35
35
  }
36
+ export function quietMessagingEnabled(config) {
37
+ return config?.quiet_messaging ?? BUILT_IN_PROJECT_YML.quiet_messaging;
38
+ }
39
+ export function leadSidebarEnabled(config) {
40
+ return config?.lead_sidebar ?? BUILT_IN_PROJECT_YML.lead_sidebar;
41
+ }
36
42
  // First entry is the default; absent or empty means claude only (todo 526).
37
43
  export function allowedAgents(config) {
38
44
  const list = config?.agents;
@@ -184,8 +190,10 @@ function applyLayer(root, path, source, config, sources, warnings) {
184
190
  if (value.auto_handoff !== undefined) {
185
191
  if (typeof value.auto_handoff === "boolean")
186
192
  budget.auto_handoff = value.auto_handoff;
187
- else
193
+ else {
194
+ budget.auto_handoff = false;
188
195
  warn(`lead_turn_budget.auto_handoff must be true or false; ignoring "${String(value.auto_handoff)}", so automatic handoff stays off.`);
196
+ }
189
197
  }
190
198
  config.lead_turn_budget = budget;
191
199
  sources.lead_turn_budget = source;
@@ -210,7 +218,7 @@ function applyLayer(root, path, source, config, sources, warnings) {
210
218
  if (has("lead_sidebar")) {
211
219
  const value = root.lead_sidebar;
212
220
  if (value == null) {
213
- config.lead_sidebar = false;
221
+ config.lead_sidebar = BUILT_IN_PROJECT_YML.lead_sidebar;
214
222
  sources.lead_sidebar = source;
215
223
  }
216
224
  else if (typeof value === "boolean") {
@@ -223,7 +231,7 @@ function applyLayer(root, path, source, config, sources, warnings) {
223
231
  if (has("quiet_messaging")) {
224
232
  const value = root.quiet_messaging;
225
233
  if (value == null) {
226
- config.quiet_messaging = false;
234
+ config.quiet_messaging = BUILT_IN_PROJECT_YML.quiet_messaging;
227
235
  sources.quiet_messaging = source;
228
236
  }
229
237
  else if (typeof value === "boolean") {
package/dist/scheduler.js CHANGED
@@ -23,7 +23,7 @@ import { evaluateLeadWatch, leadSubscription } from "./leadWatch.js";
23
23
  import { describeLiveTasks, describeOneTask, liveBackgroundTasks, STALL_BOUND_SECONDS, } from "./backgroundTasks.js";
24
24
  import { MESSAGE_MAX_ROWS, MESSAGE_RETENTION } from "./leadMessage.js";
25
25
  import { PANE_LEASE_SETTLE_MS, releasePaneLease, tryAcquirePaneLease } from "./leaseStore.js";
26
- import { CROSS_SESSION_CLOSE, postClaudeWake, promptMarkerEvidenceSql, quietMessagingOn, socketGraceDue, socketSenderAddress, SOCKET_WAKE_FOOTER, } from "./claudeWake.js";
26
+ import { CROSS_SESSION_CLOSE, quietMessagingHooksLaunched, postClaudeWake, promptMarkerEvidenceSql, quietMessagingOn, socketGraceDue, socketSenderAddress, SOCKET_WAKE_FOOTER, } from "./claudeWake.js";
27
27
  import { driveLeadHandoff } from "./leadHandoff.js";
28
28
  import { handoffHoldsWake } from "./leadHandoffState.js";
29
29
  import { FIRST_MESSAGE_LOG_STATE, humanPromptSql, readLeadTurnState } from "./leadState.js";
@@ -1949,7 +1949,8 @@ function socketRoute(timer, owner) {
1949
1949
  timer.deliver_messaging_pane_pid !== "" &&
1950
1950
  timer.deliver_messaging_pane_pid === timer.deliver_pane_pid &&
1951
1951
  timer.deliver_row_target === timer.deliver_pane &&
1952
- quietMessagingOn(timer.project_id));
1952
+ quietMessagingOn(timer.project_id) &&
1953
+ quietMessagingHooksLaunched(timer.project_id, timer.deliver_command));
1953
1954
  }
1954
1955
  function socketStillRegistered(timer, owner) {
1955
1956
  requireStillOwned(owner.rowId, owner.identity, `wake #${timer.id} socket delivery`);
package/dist/version.js CHANGED
@@ -59,6 +59,9 @@ function packageVersion() {
59
59
  return "0.0.0";
60
60
  }
61
61
  }
62
+ export function hiveVersion() {
63
+ return readBuildInfo()?.version ?? packageVersion();
64
+ }
62
65
  function currentGit() {
63
66
  const sha = gitOut(checkoutRoot, ["rev-parse", "--short", "HEAD"]);
64
67
  const status = gitOut(checkoutRoot, ["status", "--porcelain"]);
@@ -67,7 +70,7 @@ function currentGit() {
67
70
  const describe = (sha, dirty) => `${sha}${dirty ? "-dirty" : ""}`;
68
71
  export function versionInfo() {
69
72
  const info = readBuildInfo();
70
- const version = info?.version ?? packageVersion();
73
+ const version = hiveVersion();
71
74
  if (!info || info.sha == null) {
72
75
  const now = currentGit();
73
76
  return now
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmgmyr/hive",
3
- "version": "1.11.7",
3
+ "version": "1.11.9",
4
4
  "description": "Run a crew of Claude Code and Codex workers from one lead session: visible tmux panes, exact state from hooks, one local SQLite store.",
5
5
  "type": "module",
6
6
  "bin": {