@hiveai/cli 0.13.9 → 0.14.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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command58 } from "commander";
4
+ import { Command as Command59 } from "commander";
5
5
 
6
6
  // src/commands/briefing.ts
7
7
  import { existsSync as existsSync3 } from "fs";
@@ -199,7 +199,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
199
199
  if (!f) continue;
200
200
  counts.set(f, (counts.get(f) ?? 0) + 1);
201
201
  }
202
- let entries = [...counts.entries()].map(([path55, changes]) => ({ path: path55, changes }));
202
+ let entries = [...counts.entries()].map(([path56, changes]) => ({ path: path56, changes }));
203
203
  const lowerPaths = filePaths.map((p) => p.toLowerCase());
204
204
  if (lowerPaths.length > 0) {
205
205
  entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
@@ -3019,7 +3019,7 @@ ${SEED_FOOTER(stack)}` });
3019
3019
  }
3020
3020
 
3021
3021
  // src/commands/init.ts
3022
- var HAIVE_GITHUB_ACTION_REF = `v${"0.13.9"}`;
3022
+ var HAIVE_GITHUB_ACTION_REF = `v${"0.14.0"}`;
3023
3023
  var PROJECT_CONTEXT_TEMPLATE = `# Project context
3024
3024
 
3025
3025
  > Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
@@ -4089,10 +4089,13 @@ import {
4089
4089
  literalMatchesAnyToken as literalMatchesAnyToken22,
4090
4090
  loadCodeMap as loadCodeMap5,
4091
4091
  loadConfig as loadConfig3,
4092
+ hashProjectContext,
4092
4093
  loadMemoriesFromDir as loadMemoriesFromDir15,
4093
4094
  loadUsageIndex as loadUsageIndex8,
4094
4095
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
4096
+ projectContextRecentlyEmitted,
4095
4097
  rankMemoriesLexical as rankMemoriesLexical2,
4098
+ recordProjectContextEmission,
4096
4099
  queryCodeMap as queryCodeMap2,
4097
4100
  resolveBriefingBudget as resolveBriefingBudget2,
4098
4101
  serializeMemory as serializeMemory10,
@@ -5711,6 +5714,9 @@ var GetBriefingInputSchema = {
5711
5714
  ),
5712
5715
  max_memories: z19.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
5713
5716
  include_project_context: z19.boolean().default(true),
5717
+ dedupe_project_context: z19.boolean().optional().describe(
5718
+ "Token saver (default ON): skip re-emitting the project-context body if an identical copy was already sent within the last few minutes this session (the agent still has it). Set false to always include it."
5719
+ ),
5714
5720
  include_module_contexts: z19.boolean().default(true),
5715
5721
  semantic: z19.boolean().default(true).describe(
5716
5722
  "Use semantic ranking when a task is provided (requires `haive embeddings index`)."
@@ -5918,7 +5924,17 @@ async function getBriefing(input, ctx) {
5918
5924
  }
5919
5925
  }
5920
5926
  }
5921
- const projectContextRaw = input.include_project_context && existsSync21(ctx.paths.projectContext) ? await readFile52(ctx.paths.projectContext, "utf8") : "";
5927
+ let projectContextRaw = input.include_project_context && existsSync21(ctx.paths.projectContext) ? await readFile52(ctx.paths.projectContext, "utf8") : "";
5928
+ let contextOmittedRecent = false;
5929
+ if (projectContextRaw && input.dedupe_project_context !== false) {
5930
+ const ctxHash = hashProjectContext(projectContextRaw);
5931
+ if (await projectContextRecentlyEmitted(ctx.paths, ctxHash)) {
5932
+ contextOmittedRecent = true;
5933
+ projectContextRaw = "";
5934
+ } else {
5935
+ await recordProjectContextEmission(ctx.paths, ctxHash);
5936
+ }
5937
+ }
5922
5938
  const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
5923
5939
  const setupWarnings = [];
5924
5940
  let autoContextGenerated = false;
@@ -6186,7 +6202,11 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
6186
6202
  search_mode: searchMode,
6187
6203
  inferred_modules: inferred,
6188
6204
  ...lastSession ? { last_session: lastSession } : {},
6189
- project_context: adaptiveTrim ? {
6205
+ project_context: contextOmittedRecent ? {
6206
+ content: "(project context unchanged \u2014 omitted to save tokens; it was provided earlier this session. Pass dedupe_project_context:false to force a full copy.)",
6207
+ truncated: false,
6208
+ omitted_recent: true
6209
+ } : adaptiveTrim ? {
6190
6210
  content: "(adaptive briefing: auto-generated context omitted \u2014 no team-specific policy matched, so a capable model needs nothing extra here)",
6191
6211
  truncated: false,
6192
6212
  ...isTemplateContext && !autoContextGenerated ? { is_template: true } : {},
@@ -7504,7 +7524,7 @@ async function patternDetect(input, ctx) {
7504
7524
  for (const [p, { count, tools }] of pathCounts) {
7505
7525
  if (count < HOT_FILE_MIN) continue;
7506
7526
  if (tools.has("mem_tried") || tools.has("mem_observe")) continue;
7507
- if (CONFIG_PATTERNS.some((cp) => path122.basename(p).includes(cp))) continue;
7527
+ if (CONFIG_PATTERNS.some((cp2) => path122.basename(p).includes(cp2))) continue;
7508
7528
  const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
7509
7529
  matches.push({
7510
7530
  kind: "hot_file",
@@ -7938,7 +7958,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7938
7958
  };
7939
7959
  }
7940
7960
  var SERVER_NAME = "haive";
7941
- var SERVER_VERSION = "0.13.9";
7961
+ var SERVER_VERSION = "0.14.0";
7942
7962
  function jsonResult(data) {
7943
7963
  return {
7944
7964
  content: [
@@ -8687,6 +8707,7 @@ function createHaiveServer(options = {}) {
8687
8707
  "anti_patterns_check",
8688
8708
  [
8689
8709
  "Scan a diff (or set of paths) against documented attempt/gotcha memories.",
8710
+ "[Diff-scan layer: the MEMORY-MATCH component. `pre_commit_check` combines this with sensors + stale checks; `haive enforce check` is the gate.]",
8690
8711
  "Surfaces 'you are about to repeat a known mistake' warnings BEFORE you commit.",
8691
8712
  "",
8692
8713
  "USE BEFORE finalizing a non-trivial change. Cheap and high-signal: the only",
@@ -8830,6 +8851,7 @@ function createHaiveServer(options = {}) {
8830
8851
  "pre_commit_check",
8831
8852
  [
8832
8853
  "One-shot 'should I block this commit?' check. Combines three signals:",
8854
+ "[Diff-scan layer: the COMBINED check (sensors + anti-patterns + stale). `haive enforce check` is the gate that runs this at commit time.]",
8833
8855
  "",
8834
8856
  " 1. anti_patterns_check \u2014 known gotchas/attempts that match the diff",
8835
8857
  " 2. mem_for_files \u2014 conventions/decisions anchored to touched files",
@@ -13308,8 +13330,8 @@ function registerDoctor(program2) {
13308
13330
  fix: "haive init"
13309
13331
  });
13310
13332
  } else {
13311
- const { readFile: readFile26 } = await import("fs/promises");
13312
- const content = await readFile26(paths.projectContext, "utf8");
13333
+ const { readFile: readFile27 } = await import("fs/promises");
13334
+ const content = await readFile27(paths.projectContext, "utf8");
13313
13335
  const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
13314
13336
  if (isTemplate) {
13315
13337
  findings.push({
@@ -13483,8 +13505,8 @@ function registerDoctor(program2) {
13483
13505
  let hasClaudeEnforcement = false;
13484
13506
  if (existsSync68(claudeSettings)) {
13485
13507
  try {
13486
- const { readFile: readFile26 } = await import("fs/promises");
13487
- const raw = await readFile26(claudeSettings, "utf8");
13508
+ const { readFile: readFile27 } = await import("fs/promises");
13509
+ const raw = await readFile27(claudeSettings, "utf8");
13488
13510
  hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
13489
13511
  } catch {
13490
13512
  hasClaudeEnforcement = false;
@@ -13507,7 +13529,7 @@ function registerDoctor(program2) {
13507
13529
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
13508
13530
  });
13509
13531
  }
13510
- findings.push(...await collectInstallFindings(root, "0.13.9"));
13532
+ findings.push(...await collectInstallFindings(root, "0.14.0"));
13511
13533
  findings.push(...await collectToolchainFindings(root));
13512
13534
  try {
13513
13535
  const legacyRaw = execSync3("haive-mcp --version", {
@@ -13515,7 +13537,7 @@ function registerDoctor(program2) {
13515
13537
  timeout: 3e3,
13516
13538
  stdio: ["ignore", "pipe", "ignore"]
13517
13539
  }).trim();
13518
- const cliVersion = "0.13.9";
13540
+ const cliVersion = "0.14.0";
13519
13541
  if (legacyRaw && legacyRaw !== cliVersion) {
13520
13542
  findings.push({
13521
13543
  severity: "warn",
@@ -14748,52 +14770,73 @@ ${briefing.project_context.content.slice(0, 1800)}`);
14748
14770
  [setup warning] ${warning}`);
14749
14771
  }
14750
14772
  });
14751
- enforce.command("pre-tool-use").description("Claude Code PreToolUse hook: block writes until hAIve briefing has been loaded.").option("-d, --dir <dir>", "project root").action(async (opts) => {
14773
+ enforce.command("pre-tool-use").description("Claude Code PreToolUse hook: surface the relevant team policy for the edited file (advise; configurable to block).").option("-d, --dir <dir>", "project root").action(async (opts) => {
14752
14774
  const payload = await readHookPayload();
14753
14775
  const root = resolveRoot(opts.dir, payload);
14754
14776
  if (!root) return;
14755
14777
  const paths = resolveHaivePaths48(root);
14756
14778
  if (!existsSync75(paths.haiveDir)) return;
14757
14779
  if (!isWriteLikeTool(payload)) return;
14758
- const ok = await hasRecentBriefingMarker2(paths, payload.session_id);
14759
- if (ok) {
14760
- const targetFiles = extractToolPaths(payload, root);
14761
- if (targetFiles.length === 0) return;
14762
- const missing = await missingRequiredMemoriesForFiles(paths, targetFiles, payload.session_id);
14763
- if (missing.length === 0) return;
14764
- const ids = missing.slice(0, 6).map((memory2) => memory2.memory.frontmatter.id);
14780
+ const config = await loadConfig13(paths);
14781
+ if (config.enforcement?.requireBriefingFirst === false) return;
14782
+ const gate = config.enforcement?.preEditGate ?? "advise";
14783
+ const targetFiles = extractToolPaths(payload, root);
14784
+ const hasMarker = await hasRecentBriefingMarker2(paths, payload.session_id);
14785
+ const missing = targetFiles.length > 0 ? await missingRequiredMemoriesForFiles(paths, targetFiles, payload.session_id) : [];
14786
+ if (hasMarker && missing.length === 0) return;
14787
+ if (targetFiles.length > 0) {
14788
+ await recordFilesIntoBriefingMarker(paths, targetFiles, missing, payload.session_id).catch(() => {
14789
+ });
14790
+ }
14791
+ const contextText = buildPreEditContext(payload.tool_name ?? "write tool", targetFiles, missing, hasMarker);
14792
+ if (gate === "block") {
14765
14793
  console.error(
14766
- [
14767
- "hAIve enforcement blocked this action.",
14768
- `Tool: ${payload.tool_name ?? "write tool"}`,
14769
- `Files: ${targetFiles.slice(0, 6).join(", ")}`,
14770
- "",
14771
- "These files have required hAIve context that was not in the current briefing:",
14772
- ...ids.map((id) => ` - ${id}`),
14773
- "",
14774
- "Load the targeted briefing before editing:",
14775
- ` ${briefingCommandForFiles(targetFiles)}`
14776
- ].join("\n")
14794
+ contextText + '\n\nThe relevant context is now recorded \u2014 re-issue the same edit to proceed (no `haive briefing` command needed). To make this advisory instead of blocking, set `{ "enforcement": { "preEditGate": "advise" } }` in .ai/haive.config.json.'
14777
14795
  );
14778
14796
  process.exit(2);
14779
14797
  }
14780
- const tool = payload.tool_name ?? "write tool";
14781
- console.error(
14782
- [
14783
- "hAIve enforcement blocked this action.",
14784
- `Tool: ${tool}`,
14785
- "",
14786
- "This project is initialized with hAIve. Load the team briefing before editing:",
14787
- " haive enforce session-start",
14788
- "or call MCP get_briefing / mem_relevant_to from your AI client.",
14789
- "",
14790
- "If this is intentional, a human can disable enforcement in .ai/haive.config.json:",
14791
- ' { "enforcement": { "requireBriefingFirst": false } }'
14792
- ].join("\n")
14793
- );
14794
- process.exit(2);
14798
+ emitPreToolUseContext(contextText);
14795
14799
  });
14796
14800
  }
14801
+ async function recordFilesIntoBriefingMarker(paths, files, missing, sessionId) {
14802
+ const existing = await readRecentBriefingMarker(paths, sessionId);
14803
+ const ids = new Set(existing?.memory_ids ?? []);
14804
+ for (const { memory: memory2 } of missing) ids.add(memory2.frontmatter.id);
14805
+ await writeBriefingMarker3(paths, {
14806
+ sessionId,
14807
+ task: existing?.task ?? "pre-edit auto-briefing",
14808
+ source: "haive-pre-edit",
14809
+ files,
14810
+ memoryIds: [...ids]
14811
+ });
14812
+ }
14813
+ function buildPreEditContext(tool, files, missing, hasMarker) {
14814
+ const lines = ["hAIve \u2014 relevant team policy for this edit", `Tool: ${tool}`];
14815
+ if (files.length > 0) lines.push(`Files: ${files.slice(0, 6).join(", ")}`);
14816
+ if (missing.length > 0) {
14817
+ lines.push("", "Consult these before editing (anchored to the files you are touching):");
14818
+ for (const { memory: memory2 } of missing.slice(0, 5)) {
14819
+ const fm = memory2.frontmatter;
14820
+ lines.push("", `### ${fm.id} (${fm.scope}/${fm.type})`, memory2.body.trim().slice(0, 900));
14821
+ }
14822
+ } else if (!hasMarker) {
14823
+ lines.push(
14824
+ "",
14825
+ "No team briefing was loaded yet this session. Proceeding \u2014 but for substantive work call get_briefing / mem_relevant_to for richer context."
14826
+ );
14827
+ }
14828
+ return lines.join("\n");
14829
+ }
14830
+ function emitPreToolUseContext(text) {
14831
+ console.log(
14832
+ JSON.stringify({
14833
+ hookSpecificOutput: {
14834
+ hookEventName: "PreToolUse",
14835
+ additionalContext: text
14836
+ }
14837
+ })
14838
+ );
14839
+ }
14797
14840
  async function buildFinishReport(dir) {
14798
14841
  const root = findProjectRoot52(dir);
14799
14842
  const paths = resolveHaivePaths48(root);
@@ -15124,7 +15167,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
15124
15167
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
15125
15168
  });
15126
15169
  }
15127
- findings.push(...await inspectIntegrationVersions(root, "0.13.9"));
15170
+ findings.push(...await inspectIntegrationVersions(root, "0.14.0"));
15128
15171
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
15129
15172
  const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
15130
15173
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -15242,7 +15285,7 @@ async function verifyMemoryPolicy(paths, config) {
15242
15285
  }
15243
15286
  async function verifyDecisionCoverage(paths, stage, sessionId) {
15244
15287
  if (!existsSync75(paths.memoriesDir)) return [];
15245
- const changedFiles = await getChangedFiles(paths.root, stage);
15288
+ const changedFiles = (await getChangedFiles(paths.root, stage)).filter((f) => !isGeneratedArtifact(f));
15246
15289
  if (changedFiles.length === 0) {
15247
15290
  return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
15248
15291
  }
@@ -16039,8 +16082,10 @@ async function missingRequiredMemoriesForFiles(paths, files, sessionId) {
16039
16082
  return memoryMatchesAnchorPaths6(memory2, files);
16040
16083
  }).map(({ memory: memory2, filePath }) => ({ memory: memory2, filePath }));
16041
16084
  }
16042
- function briefingCommandForFiles(files) {
16043
- return `haive briefing --files "${files.slice(0, 10).join(",")}" --task "edit ${files.slice(0, 3).join(", ")}"`;
16085
+ function isGeneratedArtifact(file) {
16086
+ if (file === ".ai/project-context.md" || file === ".ai/code-map.json") return true;
16087
+ if (file.startsWith(".ai/.cache/") || file.startsWith(".ai/.runtime/") || file.startsWith(".ai/.usage/")) return true;
16088
+ return false;
16044
16089
  }
16045
16090
  async function readStdin2(maxBytes) {
16046
16091
  if (process.stdin.isTTY) return "";
@@ -16151,7 +16196,9 @@ function registerSensors(program2) {
16151
16196
  if (row.last_fired) console.log(` ${ui.dim("last fired:")} ${row.last_fired}`);
16152
16197
  }
16153
16198
  });
16154
- sensors.command("check").description("Run regex sensors against a diff; defaults to `git diff --cached`").option("--diff-file <path>", "read unified diff from a file instead of staged changes").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
16199
+ sensors.command("check").description(
16200
+ "Run regex sensors against a diff (the deterministic/computational layer); defaults to `git diff --cached`.\n Diff-scan layers: `sensors check` (regex) and `anti_patterns_check` (memory match) are components;\n `pre_commit_check` combines them; `haive enforce check` is THE gate that runs at commit."
16201
+ ).option("--diff-file <path>", "read unified diff from a file instead of staged changes").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
16155
16202
  const root = findProjectRoot53(opts.dir);
16156
16203
  const paths = resolveHaivePaths49(root);
16157
16204
  const memories = await runnableSensorMemories(paths);
@@ -16634,9 +16681,72 @@ function warnNum(n) {
16634
16681
  return n > 0 ? ui.yellow(String(n)) : String(n);
16635
16682
  }
16636
16683
 
16684
+ // src/commands/dev-link.ts
16685
+ import { execFile as execFile3 } from "child_process";
16686
+ import { cp, readFile as readFile26 } from "fs/promises";
16687
+ import { existsSync as existsSync79 } from "fs";
16688
+ import path55 from "path";
16689
+ import { promisify as promisify3 } from "util";
16690
+ import "commander";
16691
+ import { findProjectRoot as findProjectRoot56 } from "@hiveai/core";
16692
+ var exec3 = promisify3(execFile3);
16693
+ function registerDevLink(program2) {
16694
+ const dev = program2.commands.find((c) => c.name() === "dev") ?? program2.command("dev").description("Developer utilities for working on hAIve itself.");
16695
+ dev.command("link").description("Hot-swap this repo's built dist into the global @hiveai install so `haive` runs your local code.").option("-d, --dir <dir>", "repo root (default: discovered from cwd)").option("--json", "emit a machine-readable summary", false).action(async (opts) => {
16696
+ const root = findProjectRoot56(opts.dir);
16697
+ if (!existsSync79(path55.join(root, "packages", "cli", "dist", "index.js"))) {
16698
+ ui.error(`Not the hAIve monorepo (no packages/cli/dist) at ${root}. Run \`pnpm -r build\` first, or pass --dir.`);
16699
+ process.exitCode = 1;
16700
+ return;
16701
+ }
16702
+ let globalModules;
16703
+ try {
16704
+ globalModules = (await exec3("npm", ["root", "-g"])).stdout.trim();
16705
+ } catch {
16706
+ globalModules = path55.join(path55.dirname(path55.dirname(process.execPath)), "lib", "node_modules");
16707
+ }
16708
+ const globalHive = path55.join(globalModules, "@hiveai");
16709
+ if (!existsSync79(globalHive)) {
16710
+ ui.error(`No global @hiveai install at ${globalHive}. Install once with \`npm i -g @hiveai/cli\`, then re-run.`);
16711
+ process.exitCode = 1;
16712
+ return;
16713
+ }
16714
+ const linked = [];
16715
+ const copyDist = async (fromPkg, toDistDir) => {
16716
+ const from = path55.join(root, "packages", fromPkg, "dist");
16717
+ if (!existsSync79(from) || !existsSync79(path55.dirname(toDistDir))) return;
16718
+ await cp(from, toDistDir, { recursive: true });
16719
+ linked.push(path55.relative(globalModules, toDistDir));
16720
+ };
16721
+ for (const pkg of ["cli", "mcp"]) {
16722
+ await copyDist(pkg, path55.join(globalHive, pkg, "dist"));
16723
+ for (const nested of ["core", "embeddings"]) {
16724
+ await copyDist(nested, path55.join(globalHive, pkg, "node_modules", "@hiveai", nested, "dist"));
16725
+ }
16726
+ }
16727
+ await copyDist("core", path55.join(globalHive, "core", "dist"));
16728
+ let version = "unknown";
16729
+ try {
16730
+ version = JSON.parse(await readFile26(path55.join(root, "package.json"), "utf8")).version ?? "unknown";
16731
+ } catch {
16732
+ }
16733
+ if (opts.json) {
16734
+ console.log(JSON.stringify({ ok: linked.length > 0, version, global_root: globalHive, linked }, null, 2));
16735
+ return;
16736
+ }
16737
+ if (linked.length === 0) {
16738
+ ui.warn("Nothing linked \u2014 no matching dist targets were found in the global install.");
16739
+ return;
16740
+ }
16741
+ ui.success(`Linked local dist (v${version}) into the global @hiveai install:`);
16742
+ for (const t of linked) console.log(` ${ui.dim("\u2192")} ${t}`);
16743
+ console.log(ui.dim("The global `haive` now runs your local build (git hooks + MCP included)."));
16744
+ });
16745
+ }
16746
+
16637
16747
  // src/index.ts
16638
- var program = new Command58();
16639
- program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.13.9").option("--advanced", "show maintenance and experimental commands in help");
16748
+ var program = new Command59();
16749
+ program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.14.0").option("--advanced", "show maintenance and experimental commands in help");
16640
16750
  registerInit(program);
16641
16751
  registerWelcome(program);
16642
16752
  registerResolveProject(program);
@@ -16647,6 +16757,7 @@ registerAgent(program);
16647
16757
  registerSensors(program);
16648
16758
  registerIngest(program);
16649
16759
  registerDashboard(program);
16760
+ registerDevLink(program);
16650
16761
  registerMcp(program);
16651
16762
  registerBriefing(program);
16652
16763
  registerTui(program);