@hiveai/cli 0.10.2 → 0.10.4

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
@@ -169,6 +169,21 @@ Strict mode checks for:
169
169
 
170
170
  `haive enforce check` prints an enforcement score and fails strict gates when the score drops below the configured threshold.
171
171
 
172
+ ### `haive sensors`
173
+
174
+ Operate executable regex sensors stored on `gotcha`/`attempt` memories.
175
+
176
+ ```bash
177
+ haive sensors list
178
+ haive sensors check # scans git diff --cached
179
+ haive sensors check --diff-file diff.patch --json
180
+ haive sensors export --format grep
181
+ ```
182
+
183
+ Autogenerated sensors are conservative: they start as `warn` and `autogen: true`. A human can promote
184
+ high-confidence sensors to `severity: block`, which makes a deterministic pre-commit blocker when the
185
+ sensor matches added diff lines.
186
+
172
187
  ### `haive benchmark`
173
188
 
174
189
  Turn hAIve-vs-plain agent trials into a repeatable demo/report.
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 Command52 } from "commander";
4
+ import { Command as Command53 } 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(([path51, changes]) => ({ path: path51, changes }));
202
+ let entries = [...counts.entries()].map(([path53, changes]) => ({ path: path53, 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)));
@@ -3623,7 +3623,8 @@ import {
3623
3623
  loadConfig as loadConfig2,
3624
3624
  loadMemoriesFromDir as loadMemoriesFromDir22,
3625
3625
  memoryFilePath as memoryFilePath2,
3626
- serializeMemory as serializeMemory3
3626
+ serializeMemory as serializeMemory3,
3627
+ suggestSensorFromMemory
3627
3628
  } from "@hiveai/core";
3628
3629
  import { z as z4 } from "zod";
3629
3630
  import { existsSync as existsSync52 } from "fs";
@@ -3712,7 +3713,8 @@ import path52 from "path";
3712
3713
  import {
3713
3714
  buildFrontmatter as buildFrontmatter22,
3714
3715
  memoryFilePath as memoryFilePath22,
3715
- serializeMemory as serializeMemory6
3716
+ serializeMemory as serializeMemory6,
3717
+ suggestSensorFromMemory as suggestSensorFromMemory2
3716
3718
  } from "@hiveai/core";
3717
3719
  import { z as z14 } from "zod";
3718
3720
  import { mkdir as mkdir42, writeFile as writeFile82 } from "fs/promises";
@@ -3803,6 +3805,7 @@ import {
3803
3805
  import { z as z23 } from "zod";
3804
3806
  import { existsSync as existsSync222 } from "fs";
3805
3807
  import {
3808
+ addedLinesFromDiff,
3806
3809
  deriveConfidence as deriveConfidence6,
3807
3810
  getUsage as getUsage7,
3808
3811
  isRetiredMemory as isRetiredMemory2,
@@ -3810,6 +3813,8 @@ import {
3810
3813
  loadUsageIndex as loadUsageIndex9,
3811
3814
  literalMatchesAnyToken as literalMatchesAnyToken3,
3812
3815
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths4,
3816
+ runSensors,
3817
+ sensorTargetsFromDiff,
3813
3818
  tokenizeQuery as tokenizeQuery3
3814
3819
  } from "@hiveai/core";
3815
3820
  import { z as z24 } from "zod";
@@ -4068,6 +4073,8 @@ async function memSave(input, ctx) {
4068
4073
  symbols: input.symbols.length ? input.symbols : fm.anchor.symbols
4069
4074
  }
4070
4075
  };
4076
+ const suggestedSensor = !newFrontmatter.sensor ? suggestSensorForSavedMemory(input.type, input.body, newFrontmatter.anchor.paths) : null;
4077
+ if (suggestedSensor) newFrontmatter.sensor = suggestedSensor;
4071
4078
  await writeFile22(
4072
4079
  topicMatch.filePath,
4073
4080
  serializeMemory3({ frontmatter: newFrontmatter, body: input.body }),
@@ -4086,7 +4093,8 @@ async function memSave(input, ctx) {
4086
4093
  revision_count: newFrontmatter.revision_count,
4087
4094
  ...mergedTw ? { warning: mergedTw } : {},
4088
4095
  ...bs ? { body_similar: bs } : {},
4089
- ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
4096
+ ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {},
4097
+ ...suggestedSensor ? { suggested_sensor: true } : {}
4090
4098
  };
4091
4099
  }
4092
4100
  }
@@ -4102,7 +4110,8 @@ async function memSave(input, ctx) {
4102
4110
  symbols: input.symbols,
4103
4111
  commit: input.commit,
4104
4112
  topic: input.topic,
4105
- status: haiveConfig.defaultStatus === "validated" ? "validated" : void 0
4113
+ status: haiveConfig.defaultStatus === "validated" ? "validated" : void 0,
4114
+ sensor: suggestSensorForSavedMemory(input.type, input.body, input.paths) ?? void 0
4106
4115
  });
4107
4116
  const file = memoryFilePath2(
4108
4117
  ctx.paths,
@@ -4143,9 +4152,14 @@ async function memSave(input, ctx) {
4143
4152
  ...finalWarning ? { warning: finalWarning } : {},
4144
4153
  ...similar_found ? { similar_found } : {},
4145
4154
  ...bsNew ? { body_similar: bsNew } : {},
4146
- ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
4155
+ ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {},
4156
+ ...frontmatter.sensor?.autogen ? { suggested_sensor: true } : {}
4147
4157
  };
4148
4158
  }
4159
+ function suggestSensorForSavedMemory(type, body, paths) {
4160
+ if (type !== "gotcha" && type !== "attempt") return null;
4161
+ return suggestSensorFromMemory(body, paths);
4162
+ }
4149
4163
  function criticalAnchorWarning(type, status, paths, symbols) {
4150
4164
  if (!["decision", "gotcha", "architecture"].includes(type)) return null;
4151
4165
  if (status !== "validated") return null;
@@ -4788,6 +4802,10 @@ async function memTried(input, ctx) {
4788
4802
  lines.push("", `**Instead, use:** ${input.instead}`);
4789
4803
  }
4790
4804
  const body = lines.join("\n") + "\n";
4805
+ const sensor = suggestSensorFromMemory2(body, input.paths);
4806
+ if (sensor) {
4807
+ frontmatter.sensor = sensor;
4808
+ }
4791
4809
  const file = memoryFilePath22(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
4792
4810
  await mkdir32(path52.dirname(file), { recursive: true });
4793
4811
  if (existsSync142(file)) {
@@ -6192,6 +6210,23 @@ async function antiPatternsCheck(input, ctx) {
6192
6210
  }
6193
6211
  }
6194
6212
  }
6213
+ if (input.diff) {
6214
+ const added = addedLinesFromDiff(input.diff);
6215
+ const diffTargets = sensorTargetsFromDiff(input.diff);
6216
+ const hasFileTargets = diffTargets.some((target) => target.path.length > 0);
6217
+ const targets = diffTargets.length > 0 && hasFileTargets ? diffTargets : input.paths.length > 0 ? input.paths.map((p) => ({ path: p, content: added.trim().length > 0 ? added : input.diff })) : [{ path: "", content: added.trim().length > 0 ? added : input.diff }];
6218
+ const hits = runSensors(negative.map(({ memory: memory2 }) => memory2), targets);
6219
+ for (const hit of hits) {
6220
+ const found = negative.find(({ memory: memory2 }) => memory2.frontmatter.id === hit.memory_id);
6221
+ if (!found) continue;
6222
+ upsert(found.memory.frontmatter, found.memory.body, "sensor");
6223
+ const w = seen.get(found.memory.frontmatter.id);
6224
+ if (w) {
6225
+ w.sensor_message = hit.message;
6226
+ w.sensor_severity = hit.severity;
6227
+ }
6228
+ }
6229
+ }
6195
6230
  if (input.semantic && input.diff) {
6196
6231
  try {
6197
6232
  const mod = await import("@hiveai/embeddings");
@@ -6210,7 +6245,7 @@ async function antiPatternsCheck(input, ctx) {
6210
6245
  }
6211
6246
  const warnings = [...seen.values()].sort((a, b) => {
6212
6247
  const score = (w) => {
6213
- const reasonW = (w.reasons.includes("anchor") ? 4 : 0) + (w.reasons.includes("literal") ? 2 : 0) + (w.reasons.includes("semantic") ? 1 : 0);
6248
+ const reasonW = (w.reasons.includes("sensor") ? 8 : 0) + (w.reasons.includes("anchor") ? 4 : 0) + (w.reasons.includes("literal") ? 2 : 0) + (w.reasons.includes("semantic") ? 1 : 0);
6214
6249
  const confW = w.confidence === "authoritative" ? 3 : w.confidence === "trusted" ? 2 : w.confidence === "low" ? 1 : 0;
6215
6250
  return reasonW + confW + (w.semantic_score ?? 0);
6216
6251
  };
@@ -6718,6 +6753,24 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
6718
6753
  repair_command: repairCommand
6719
6754
  };
6720
6755
  }
6756
+ if (warning.reasons.includes("sensor")) {
6757
+ if (warning.sensor_severity === "block") {
6758
+ return {
6759
+ ...warning,
6760
+ level: "blocking",
6761
+ rationale: "deterministic hAIve sensor with block severity matched the added diff",
6762
+ affected_files: affectedFiles,
6763
+ repair_command: repairCommand
6764
+ };
6765
+ }
6766
+ return {
6767
+ ...warning,
6768
+ level: "review",
6769
+ rationale: "deterministic hAIve sensor with warn severity matched the added diff",
6770
+ affected_files: affectedFiles,
6771
+ repair_command: repairCommand
6772
+ };
6773
+ }
6721
6774
  if (isBlockingWarning(warning)) {
6722
6775
  return {
6723
6776
  ...warning,
@@ -7389,7 +7442,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7389
7442
  };
7390
7443
  }
7391
7444
  var SERVER_NAME = "haive";
7392
- var SERVER_VERSION = "0.10.2";
7445
+ var SERVER_VERSION = "0.10.4";
7393
7446
  function jsonResult(data) {
7394
7447
  return {
7395
7448
  content: [
@@ -8908,7 +8961,8 @@ import {
8908
8961
  loadMemoriesFromDir as loadMemoriesFromDir24,
8909
8962
  memoryFilePath as memoryFilePath6,
8910
8963
  resolveHaivePaths as resolveHaivePaths10,
8911
- serializeMemory as serializeMemory12
8964
+ serializeMemory as serializeMemory12,
8965
+ suggestSensorFromMemory as suggestSensorFromMemory3
8912
8966
  } from "@hiveai/core";
8913
8967
  function registerMemoryAdd(memory2) {
8914
8968
  memory2.command("add").description(
@@ -9010,9 +9064,12 @@ TODO \u2014 write the memory body.
9010
9064
  symbols: parseCsv2(opts.symbols).length ? parseCsv2(opts.symbols) : fm.anchor.symbols
9011
9065
  }
9012
9066
  };
9067
+ const suggestedSensor = !newFrontmatter.sensor ? suggestSensorForCliMemory(opts.type, body, newFrontmatter.anchor.paths) : null;
9068
+ if (suggestedSensor) newFrontmatter.sensor = suggestedSensor;
9013
9069
  await writeFile14(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
9014
9070
  ui.success(`Updated (topic upsert) ${path16.relative(root, topicMatch.filePath)}`);
9015
9071
  ui.info(`id=${fm.id} revision=${revisionCount}`);
9072
+ if (suggestedSensor) ui.info(`sensor=regex warn autogen pattern=${JSON.stringify(suggestedSensor.pattern)}`);
9016
9073
  await runPostMemoryAutopilot(root, paths, config);
9017
9074
  return;
9018
9075
  }
@@ -9029,7 +9086,8 @@ TODO \u2014 write the memory body.
9029
9086
  symbols: parseCsv2(opts.symbols),
9030
9087
  commit: opts.commit,
9031
9088
  topic: opts.topic,
9032
- status: config.defaultStatus === "validated" ? "validated" : void 0
9089
+ status: config.defaultStatus === "validated" ? "validated" : void 0,
9090
+ sensor: suggestSensorForCliMemory(opts.type, body, anchorPaths) ?? void 0
9033
9091
  });
9034
9092
  const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
9035
9093
  await mkdir11(path16.dirname(file), { recursive: true });
@@ -9053,6 +9111,9 @@ TODO \u2014 write the memory body.
9053
9111
  await writeFile14(file, serializeMemory12({ frontmatter, body }), "utf8");
9054
9112
  ui.success(`Created ${path16.relative(root, file)}`);
9055
9113
  ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
9114
+ if (frontmatter.sensor?.autogen) {
9115
+ ui.info(`sensor=regex warn autogen pattern=${JSON.stringify(frontmatter.sensor.pattern)}`);
9116
+ }
9056
9117
  await runPostMemoryAutopilot(root, paths, config);
9057
9118
  if (inferredTags.length > 0) {
9058
9119
  ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
@@ -9096,6 +9157,10 @@ function parseCsv2(value) {
9096
9157
  if (!value) return [];
9097
9158
  return value.split(",").map((s) => s.trim()).filter(Boolean);
9098
9159
  }
9160
+ function suggestSensorForCliMemory(type, body, anchorPaths) {
9161
+ if (type !== "gotcha" && type !== "attempt") return null;
9162
+ return suggestSensorFromMemory3(body, anchorPaths);
9163
+ }
9099
9164
  function normalizeBody(rawBody, title, titleExplicit) {
9100
9165
  const trimmed = rawBody.trim();
9101
9166
  if (/^#{1,3}\s+\S/m.test(trimmed)) return `${trimmed}
@@ -9751,7 +9816,8 @@ import {
9751
9816
  findProjectRoot as findProjectRoot22,
9752
9817
  memoryFilePath as memoryFilePath8,
9753
9818
  resolveHaivePaths as resolveHaivePaths19,
9754
- serializeMemory as serializeMemory17
9819
+ serializeMemory as serializeMemory17,
9820
+ suggestSensorFromMemory as suggestSensorFromMemory4
9755
9821
  } from "@hiveai/core";
9756
9822
  function registerMemoryTried(memory2) {
9757
9823
  memory2.command("tried").description(
@@ -9794,6 +9860,10 @@ function registerMemoryTried(memory2) {
9794
9860
  lines.push("", `**Instead, use:** ${opts.instead}`);
9795
9861
  }
9796
9862
  const body = lines.join("\n") + "\n";
9863
+ const sensor = suggestSensorFromMemory4(body, frontmatter.anchor.paths);
9864
+ if (sensor) {
9865
+ frontmatter.sensor = sensor;
9866
+ }
9797
9867
  const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
9798
9868
  await mkdir13(path26.dirname(file), { recursive: true });
9799
9869
  if (existsSync40(file)) {
@@ -9804,6 +9874,7 @@ function registerMemoryTried(memory2) {
9804
9874
  await writeFile19(file, serializeMemory17({ frontmatter, body }), "utf8");
9805
9875
  ui.success(`Recorded: ${path26.relative(root, file)}`);
9806
9876
  ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
9877
+ if (sensor) ui.info(`sensor=regex warn autogen pattern=${JSON.stringify(sensor.pattern)}`);
9807
9878
  });
9808
9879
  }
9809
9880
  function parseCsv4(value) {
@@ -12277,8 +12348,8 @@ function registerDoctor(program2) {
12277
12348
  fix: "haive init"
12278
12349
  });
12279
12350
  } else {
12280
- const { readFile: readFile23 } = await import("fs/promises");
12281
- const content = await readFile23(paths.projectContext, "utf8");
12351
+ const { readFile: readFile24 } = await import("fs/promises");
12352
+ const content = await readFile24(paths.projectContext, "utf8");
12282
12353
  const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
12283
12354
  if (isTemplate) {
12284
12355
  findings.push({
@@ -12443,8 +12514,8 @@ function registerDoctor(program2) {
12443
12514
  let hasClaudeEnforcement = false;
12444
12515
  if (existsSync61(claudeSettings)) {
12445
12516
  try {
12446
- const { readFile: readFile23 } = await import("fs/promises");
12447
- const raw = await readFile23(claudeSettings, "utf8");
12517
+ const { readFile: readFile24 } = await import("fs/promises");
12518
+ const raw = await readFile24(claudeSettings, "utf8");
12448
12519
  hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
12449
12520
  } catch {
12450
12521
  hasClaudeEnforcement = false;
@@ -12467,14 +12538,14 @@ function registerDoctor(program2) {
12467
12538
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
12468
12539
  });
12469
12540
  }
12470
- findings.push(...await collectInstallFindings(root, "0.10.2"));
12541
+ findings.push(...await collectInstallFindings(root, "0.10.4"));
12471
12542
  try {
12472
12543
  const legacyRaw = execSync3("haive-mcp --version", {
12473
12544
  encoding: "utf8",
12474
12545
  timeout: 3e3,
12475
12546
  stdio: ["ignore", "pipe", "ignore"]
12476
12547
  }).trim();
12477
- const cliVersion = "0.10.2";
12548
+ const cliVersion = "0.10.4";
12478
12549
  if (legacyRaw && legacyRaw !== cliVersion) {
12479
12550
  findings.push({
12480
12551
  severity: "warn",
@@ -13772,7 +13843,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
13772
13843
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
13773
13844
  });
13774
13845
  }
13775
- findings.push(...await inspectIntegrationVersions(root, "0.10.2"));
13846
+ findings.push(...await inspectIntegrationVersions(root, "0.10.4"));
13776
13847
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
13777
13848
  const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
13778
13849
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -14382,9 +14453,157 @@ function registerRun(program2) {
14382
14453
  });
14383
14454
  }
14384
14455
 
14456
+ // src/commands/sensors.ts
14457
+ import { execFile as execFile2 } from "child_process";
14458
+ import { existsSync as existsSync69 } from "fs";
14459
+ import { chmod as chmod3, mkdir as mkdir20, readFile as readFile23, writeFile as writeFile34 } from "fs/promises";
14460
+ import path51 from "path";
14461
+ import { promisify as promisify2 } from "util";
14462
+ import "commander";
14463
+ import {
14464
+ findProjectRoot as findProjectRoot50,
14465
+ isRetiredMemory as isRetiredMemory3,
14466
+ loadMemoriesFromDir as loadMemoriesFromDir37,
14467
+ resolveHaivePaths as resolveHaivePaths46,
14468
+ runSensors as runSensors2,
14469
+ sensorTargetsFromDiff as sensorTargetsFromDiff2
14470
+ } from "@hiveai/core";
14471
+ var exec2 = promisify2(execFile2);
14472
+ function registerSensors(program2) {
14473
+ const sensors = program2.command("sensors").description("Operate executable sensors derived from hAIve memories");
14474
+ sensors.command("list").description("List memories carrying executable sensors").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
14475
+ const root = findProjectRoot50(opts.dir);
14476
+ const paths = resolveHaivePaths46(root);
14477
+ const rows = await sensorRows(paths);
14478
+ if (opts.json) {
14479
+ console.log(JSON.stringify(rows, null, 2));
14480
+ return;
14481
+ }
14482
+ if (rows.length === 0) {
14483
+ ui.warn("No sensors found.");
14484
+ return;
14485
+ }
14486
+ console.log(ui.bold(`hAIve sensors \u2014 ${rows.length}`));
14487
+ for (const row of rows) {
14488
+ console.log(
14489
+ ` \u2022 ${row.id} ${ui.dim(`(${row.kind}, ${row.severity})`)} ${row.pattern ?? row.command ?? ""}`
14490
+ );
14491
+ if (row.paths.length > 0) console.log(` ${ui.dim("paths:")} ${row.paths.join(", ")}`);
14492
+ if (row.last_fired) console.log(` ${ui.dim("last fired:")} ${row.last_fired}`);
14493
+ }
14494
+ });
14495
+ 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) => {
14496
+ const root = findProjectRoot50(opts.dir);
14497
+ const paths = resolveHaivePaths46(root);
14498
+ const memories = await runnableSensorMemories(paths);
14499
+ const diff = opts.diffFile ? await readFile23(path51.resolve(root, opts.diffFile), "utf8") : await stagedDiff(root);
14500
+ const targets = sensorTargetsFromDiff2(diff);
14501
+ const hits = runSensors2(memories, targets.length > 0 ? targets : [{ path: "", content: diff }]);
14502
+ const output = {
14503
+ scanned: memories.length,
14504
+ hits: hits.map((hit) => ({
14505
+ memory_id: hit.memory_id,
14506
+ file: hit.file,
14507
+ severity: hit.severity,
14508
+ message: hit.message,
14509
+ matched_line: hit.matched_line
14510
+ }))
14511
+ };
14512
+ if (opts.json) {
14513
+ console.log(JSON.stringify(output, null, 2));
14514
+ } else {
14515
+ console.log(ui.bold(`hAIve sensors check \u2014 ${hits.length} hit(s), ${memories.length} sensor(s)`));
14516
+ for (const hit of hits) {
14517
+ const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
14518
+ console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity})`)}`);
14519
+ if (hit.file) console.log(` ${ui.dim("file:")} ${hit.file}`);
14520
+ console.log(` ${hit.message}`);
14521
+ if (hit.matched_line) console.log(` ${ui.dim(hit.matched_line)}`);
14522
+ }
14523
+ }
14524
+ if (hits.some((hit) => hit.severity === "block")) process.exitCode = 1;
14525
+ });
14526
+ sensors.command("export").description("Export regex sensors into .ai/generated for external toolchains").option("--format <format>", "grep | eslint", "grep").option("--out-dir <dir>", "output directory", ".ai/generated").option("-d, --dir <dir>", "project root").action(async (opts) => {
14527
+ const format = opts.format ?? "grep";
14528
+ if (format !== "grep" && format !== "eslint") {
14529
+ ui.error("--format must be grep or eslint");
14530
+ process.exitCode = 1;
14531
+ return;
14532
+ }
14533
+ const root = findProjectRoot50(opts.dir);
14534
+ const paths = resolveHaivePaths46(root);
14535
+ const rows = await sensorRows(paths);
14536
+ const outDir = path51.resolve(root, opts.outDir ?? ".ai/generated");
14537
+ await mkdir20(outDir, { recursive: true });
14538
+ const outPath = path51.join(outDir, format === "grep" ? "haive-sensors-grep.sh" : "haive-sensors-eslint.json");
14539
+ const content = format === "grep" ? renderGrepScript(rows) : JSON.stringify({ sensors: rows }, null, 2) + "\n";
14540
+ await writeFile34(outPath, content, "utf8");
14541
+ if (format === "grep") await chmod3(outPath, 493);
14542
+ ui.success(`Exported ${rows.length} sensor(s): ${path51.relative(root, outPath)}`);
14543
+ });
14544
+ }
14545
+ async function sensorRows(paths) {
14546
+ const memories = await runnableSensorMemories(paths, false);
14547
+ return memories.map((memory2) => {
14548
+ const sensor = memory2.frontmatter.sensor;
14549
+ return {
14550
+ id: memory2.frontmatter.id,
14551
+ kind: sensor.kind,
14552
+ severity: sensor.severity,
14553
+ pattern: sensor.pattern,
14554
+ command: sensor.command,
14555
+ paths: sensor.paths.length > 0 ? sensor.paths : memory2.frontmatter.anchor.paths,
14556
+ message: sensor.message,
14557
+ autogen: sensor.autogen,
14558
+ last_fired: sensor.last_fired
14559
+ };
14560
+ });
14561
+ }
14562
+ async function runnableSensorMemories(paths, regexOnly = true) {
14563
+ if (!existsSync69(paths.memoriesDir)) return [];
14564
+ const loaded = await loadMemoriesFromDir37(paths.memoriesDir);
14565
+ return loaded.map(({ memory: memory2 }) => memory2).filter((memory2) => {
14566
+ const sensor = memory2.frontmatter.sensor;
14567
+ if (!sensor) return false;
14568
+ if (regexOnly && sensor.kind !== "regex") return false;
14569
+ return !isRetiredMemory3(memory2.frontmatter, memory2.body);
14570
+ });
14571
+ }
14572
+ async function stagedDiff(root) {
14573
+ try {
14574
+ const { stdout } = await exec2("git", ["diff", "--cached"], { cwd: root });
14575
+ return stdout;
14576
+ } catch (err) {
14577
+ throw new Error(`git diff --cached failed: ${err instanceof Error ? err.message : String(err)}`);
14578
+ }
14579
+ }
14580
+ function renderGrepScript(rows) {
14581
+ const lines = [
14582
+ "#!/usr/bin/env bash",
14583
+ "set -euo pipefail",
14584
+ "status=0",
14585
+ ""
14586
+ ];
14587
+ for (const row of rows.filter((item) => item.kind === "regex" && item.pattern)) {
14588
+ const paths = row.paths.length > 0 ? row.paths : ["."];
14589
+ for (const p of paths) {
14590
+ lines.push(`if grep -RInE -- ${shellQuote(row.pattern)} ${shellQuote(p)}; then`);
14591
+ lines.push(` echo ${shellQuote(`hAIve sensor ${row.id}: ${row.message}`)}`);
14592
+ if (row.severity === "block") lines.push(" status=1");
14593
+ lines.push("fi");
14594
+ lines.push("");
14595
+ }
14596
+ }
14597
+ lines.push("exit $status", "");
14598
+ return lines.join("\n");
14599
+ }
14600
+ function shellQuote(value) {
14601
+ return `'${value.replace(/'/g, "'\\''")}'`;
14602
+ }
14603
+
14385
14604
  // src/index.ts
14386
- var program = new Command52();
14387
- program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.10.2").option("--advanced", "show maintenance and experimental commands in help");
14605
+ var program = new Command53();
14606
+ program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.10.4").option("--advanced", "show maintenance and experimental commands in help");
14388
14607
  registerInit(program);
14389
14608
  registerWelcome(program);
14390
14609
  registerResolveProject(program);
@@ -14392,6 +14611,7 @@ registerRuntime(program);
14392
14611
  registerEnforce(program);
14393
14612
  registerRun(program);
14394
14613
  registerAgent(program);
14614
+ registerSensors(program);
14395
14615
  registerMcp(program);
14396
14616
  registerBriefing(program);
14397
14617
  registerTui(program);
@@ -14447,6 +14667,7 @@ var CORE_ROOT_COMMANDS = /* @__PURE__ */ new Set([
14447
14667
  "briefing",
14448
14668
  "enforce",
14449
14669
  "run",
14670
+ "sensors",
14450
14671
  "sync",
14451
14672
  "mcp",
14452
14673
  "memory",