@hiveai/cli 0.10.3 → 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";
@@ -3811,8 +3813,8 @@ import {
3811
3813
  loadUsageIndex as loadUsageIndex9,
3812
3814
  literalMatchesAnyToken as literalMatchesAnyToken3,
3813
3815
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths4,
3814
- runRegexSensor,
3815
- sensorAppliesToPath,
3816
+ runSensors,
3817
+ sensorTargetsFromDiff,
3816
3818
  tokenizeQuery as tokenizeQuery3
3817
3819
  } from "@hiveai/core";
3818
3820
  import { z as z24 } from "zod";
@@ -4071,6 +4073,8 @@ async function memSave(input, ctx) {
4071
4073
  symbols: input.symbols.length ? input.symbols : fm.anchor.symbols
4072
4074
  }
4073
4075
  };
4076
+ const suggestedSensor = !newFrontmatter.sensor ? suggestSensorForSavedMemory(input.type, input.body, newFrontmatter.anchor.paths) : null;
4077
+ if (suggestedSensor) newFrontmatter.sensor = suggestedSensor;
4074
4078
  await writeFile22(
4075
4079
  topicMatch.filePath,
4076
4080
  serializeMemory3({ frontmatter: newFrontmatter, body: input.body }),
@@ -4089,7 +4093,8 @@ async function memSave(input, ctx) {
4089
4093
  revision_count: newFrontmatter.revision_count,
4090
4094
  ...mergedTw ? { warning: mergedTw } : {},
4091
4095
  ...bs ? { body_similar: bs } : {},
4092
- ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
4096
+ ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {},
4097
+ ...suggestedSensor ? { suggested_sensor: true } : {}
4093
4098
  };
4094
4099
  }
4095
4100
  }
@@ -4105,7 +4110,8 @@ async function memSave(input, ctx) {
4105
4110
  symbols: input.symbols,
4106
4111
  commit: input.commit,
4107
4112
  topic: input.topic,
4108
- 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
4109
4115
  });
4110
4116
  const file = memoryFilePath2(
4111
4117
  ctx.paths,
@@ -4146,9 +4152,14 @@ async function memSave(input, ctx) {
4146
4152
  ...finalWarning ? { warning: finalWarning } : {},
4147
4153
  ...similar_found ? { similar_found } : {},
4148
4154
  ...bsNew ? { body_similar: bsNew } : {},
4149
- ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
4155
+ ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {},
4156
+ ...frontmatter.sensor?.autogen ? { suggested_sensor: true } : {}
4150
4157
  };
4151
4158
  }
4159
+ function suggestSensorForSavedMemory(type, body, paths) {
4160
+ if (type !== "gotcha" && type !== "attempt") return null;
4161
+ return suggestSensorFromMemory(body, paths);
4162
+ }
4152
4163
  function criticalAnchorWarning(type, status, paths, symbols) {
4153
4164
  if (!["decision", "gotcha", "architecture"].includes(type)) return null;
4154
4165
  if (status !== "validated") return null;
@@ -4791,6 +4802,10 @@ async function memTried(input, ctx) {
4791
4802
  lines.push("", `**Instead, use:** ${input.instead}`);
4792
4803
  }
4793
4804
  const body = lines.join("\n") + "\n";
4805
+ const sensor = suggestSensorFromMemory2(body, input.paths);
4806
+ if (sensor) {
4807
+ frontmatter.sensor = sensor;
4808
+ }
4794
4809
  const file = memoryFilePath22(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
4795
4810
  await mkdir32(path52.dirname(file), { recursive: true });
4796
4811
  if (existsSync142(file)) {
@@ -6197,24 +6212,18 @@ async function antiPatternsCheck(input, ctx) {
6197
6212
  }
6198
6213
  if (input.diff) {
6199
6214
  const added = addedLinesFromDiff(input.diff);
6200
- const scanText = added.trim().length > 0 ? added : input.diff;
6201
- for (const { memory: memory2 } of negative) {
6202
- const sensor = memory2.frontmatter.sensor;
6203
- if (!sensor || sensor.kind !== "regex") continue;
6204
- const anchorPaths = memory2.frontmatter.anchor.paths;
6205
- const inScope = input.paths.length === 0 || input.paths.some((p) => sensorAppliesToPath(sensor, anchorPaths, p));
6206
- if (!inScope) continue;
6207
- const hit = runRegexSensor(memory2.frontmatter.id, sensor, {
6208
- path: input.paths[0] ?? "",
6209
- content: scanText
6210
- });
6211
- if (hit) {
6212
- upsert(memory2.frontmatter, memory2.body, "sensor");
6213
- const w = seen.get(memory2.frontmatter.id);
6214
- if (w) {
6215
- w.sensor_message = hit.message;
6216
- w.sensor_severity = hit.severity;
6217
- }
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;
6218
6227
  }
6219
6228
  }
6220
6229
  }
@@ -6744,6 +6753,24 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
6744
6753
  repair_command: repairCommand
6745
6754
  };
6746
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
+ }
6747
6774
  if (isBlockingWarning(warning)) {
6748
6775
  return {
6749
6776
  ...warning,
@@ -7415,7 +7442,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7415
7442
  };
7416
7443
  }
7417
7444
  var SERVER_NAME = "haive";
7418
- var SERVER_VERSION = "0.10.3";
7445
+ var SERVER_VERSION = "0.10.4";
7419
7446
  function jsonResult(data) {
7420
7447
  return {
7421
7448
  content: [
@@ -8934,7 +8961,8 @@ import {
8934
8961
  loadMemoriesFromDir as loadMemoriesFromDir24,
8935
8962
  memoryFilePath as memoryFilePath6,
8936
8963
  resolveHaivePaths as resolveHaivePaths10,
8937
- serializeMemory as serializeMemory12
8964
+ serializeMemory as serializeMemory12,
8965
+ suggestSensorFromMemory as suggestSensorFromMemory3
8938
8966
  } from "@hiveai/core";
8939
8967
  function registerMemoryAdd(memory2) {
8940
8968
  memory2.command("add").description(
@@ -9036,9 +9064,12 @@ TODO \u2014 write the memory body.
9036
9064
  symbols: parseCsv2(opts.symbols).length ? parseCsv2(opts.symbols) : fm.anchor.symbols
9037
9065
  }
9038
9066
  };
9067
+ const suggestedSensor = !newFrontmatter.sensor ? suggestSensorForCliMemory(opts.type, body, newFrontmatter.anchor.paths) : null;
9068
+ if (suggestedSensor) newFrontmatter.sensor = suggestedSensor;
9039
9069
  await writeFile14(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
9040
9070
  ui.success(`Updated (topic upsert) ${path16.relative(root, topicMatch.filePath)}`);
9041
9071
  ui.info(`id=${fm.id} revision=${revisionCount}`);
9072
+ if (suggestedSensor) ui.info(`sensor=regex warn autogen pattern=${JSON.stringify(suggestedSensor.pattern)}`);
9042
9073
  await runPostMemoryAutopilot(root, paths, config);
9043
9074
  return;
9044
9075
  }
@@ -9055,7 +9086,8 @@ TODO \u2014 write the memory body.
9055
9086
  symbols: parseCsv2(opts.symbols),
9056
9087
  commit: opts.commit,
9057
9088
  topic: opts.topic,
9058
- status: config.defaultStatus === "validated" ? "validated" : void 0
9089
+ status: config.defaultStatus === "validated" ? "validated" : void 0,
9090
+ sensor: suggestSensorForCliMemory(opts.type, body, anchorPaths) ?? void 0
9059
9091
  });
9060
9092
  const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
9061
9093
  await mkdir11(path16.dirname(file), { recursive: true });
@@ -9079,6 +9111,9 @@ TODO \u2014 write the memory body.
9079
9111
  await writeFile14(file, serializeMemory12({ frontmatter, body }), "utf8");
9080
9112
  ui.success(`Created ${path16.relative(root, file)}`);
9081
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
+ }
9082
9117
  await runPostMemoryAutopilot(root, paths, config);
9083
9118
  if (inferredTags.length > 0) {
9084
9119
  ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
@@ -9122,6 +9157,10 @@ function parseCsv2(value) {
9122
9157
  if (!value) return [];
9123
9158
  return value.split(",").map((s) => s.trim()).filter(Boolean);
9124
9159
  }
9160
+ function suggestSensorForCliMemory(type, body, anchorPaths) {
9161
+ if (type !== "gotcha" && type !== "attempt") return null;
9162
+ return suggestSensorFromMemory3(body, anchorPaths);
9163
+ }
9125
9164
  function normalizeBody(rawBody, title, titleExplicit) {
9126
9165
  const trimmed = rawBody.trim();
9127
9166
  if (/^#{1,3}\s+\S/m.test(trimmed)) return `${trimmed}
@@ -9777,7 +9816,8 @@ import {
9777
9816
  findProjectRoot as findProjectRoot22,
9778
9817
  memoryFilePath as memoryFilePath8,
9779
9818
  resolveHaivePaths as resolveHaivePaths19,
9780
- serializeMemory as serializeMemory17
9819
+ serializeMemory as serializeMemory17,
9820
+ suggestSensorFromMemory as suggestSensorFromMemory4
9781
9821
  } from "@hiveai/core";
9782
9822
  function registerMemoryTried(memory2) {
9783
9823
  memory2.command("tried").description(
@@ -9820,6 +9860,10 @@ function registerMemoryTried(memory2) {
9820
9860
  lines.push("", `**Instead, use:** ${opts.instead}`);
9821
9861
  }
9822
9862
  const body = lines.join("\n") + "\n";
9863
+ const sensor = suggestSensorFromMemory4(body, frontmatter.anchor.paths);
9864
+ if (sensor) {
9865
+ frontmatter.sensor = sensor;
9866
+ }
9823
9867
  const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
9824
9868
  await mkdir13(path26.dirname(file), { recursive: true });
9825
9869
  if (existsSync40(file)) {
@@ -9830,6 +9874,7 @@ function registerMemoryTried(memory2) {
9830
9874
  await writeFile19(file, serializeMemory17({ frontmatter, body }), "utf8");
9831
9875
  ui.success(`Recorded: ${path26.relative(root, file)}`);
9832
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)}`);
9833
9878
  });
9834
9879
  }
9835
9880
  function parseCsv4(value) {
@@ -12303,8 +12348,8 @@ function registerDoctor(program2) {
12303
12348
  fix: "haive init"
12304
12349
  });
12305
12350
  } else {
12306
- const { readFile: readFile23 } = await import("fs/promises");
12307
- const content = await readFile23(paths.projectContext, "utf8");
12351
+ const { readFile: readFile24 } = await import("fs/promises");
12352
+ const content = await readFile24(paths.projectContext, "utf8");
12308
12353
  const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
12309
12354
  if (isTemplate) {
12310
12355
  findings.push({
@@ -12469,8 +12514,8 @@ function registerDoctor(program2) {
12469
12514
  let hasClaudeEnforcement = false;
12470
12515
  if (existsSync61(claudeSettings)) {
12471
12516
  try {
12472
- const { readFile: readFile23 } = await import("fs/promises");
12473
- const raw = await readFile23(claudeSettings, "utf8");
12517
+ const { readFile: readFile24 } = await import("fs/promises");
12518
+ const raw = await readFile24(claudeSettings, "utf8");
12474
12519
  hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
12475
12520
  } catch {
12476
12521
  hasClaudeEnforcement = false;
@@ -12493,14 +12538,14 @@ function registerDoctor(program2) {
12493
12538
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
12494
12539
  });
12495
12540
  }
12496
- findings.push(...await collectInstallFindings(root, "0.10.3"));
12541
+ findings.push(...await collectInstallFindings(root, "0.10.4"));
12497
12542
  try {
12498
12543
  const legacyRaw = execSync3("haive-mcp --version", {
12499
12544
  encoding: "utf8",
12500
12545
  timeout: 3e3,
12501
12546
  stdio: ["ignore", "pipe", "ignore"]
12502
12547
  }).trim();
12503
- const cliVersion = "0.10.3";
12548
+ const cliVersion = "0.10.4";
12504
12549
  if (legacyRaw && legacyRaw !== cliVersion) {
12505
12550
  findings.push({
12506
12551
  severity: "warn",
@@ -13798,7 +13843,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
13798
13843
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
13799
13844
  });
13800
13845
  }
13801
- findings.push(...await inspectIntegrationVersions(root, "0.10.3"));
13846
+ findings.push(...await inspectIntegrationVersions(root, "0.10.4"));
13802
13847
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
13803
13848
  const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
13804
13849
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -14408,9 +14453,157 @@ function registerRun(program2) {
14408
14453
  });
14409
14454
  }
14410
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
+
14411
14604
  // src/index.ts
14412
- var program = new Command52();
14413
- program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.10.3").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");
14414
14607
  registerInit(program);
14415
14608
  registerWelcome(program);
14416
14609
  registerResolveProject(program);
@@ -14418,6 +14611,7 @@ registerRuntime(program);
14418
14611
  registerEnforce(program);
14419
14612
  registerRun(program);
14420
14613
  registerAgent(program);
14614
+ registerSensors(program);
14421
14615
  registerMcp(program);
14422
14616
  registerBriefing(program);
14423
14617
  registerTui(program);
@@ -14473,6 +14667,7 @@ var CORE_ROOT_COMMANDS = /* @__PURE__ */ new Set([
14473
14667
  "briefing",
14474
14668
  "enforce",
14475
14669
  "run",
14670
+ "sensors",
14476
14671
  "sync",
14477
14672
  "mcp",
14478
14673
  "memory",