@merittdev/horus 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +2561 -475
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -16235,9 +16235,9 @@ var require_common2 = __commonJS({
16235
16235
  };
16236
16236
  exports2.Batch = Batch;
16237
16237
  var BulkWriteResult = class _BulkWriteResult {
16238
- static generateIdMap(ids) {
16238
+ static generateIdMap(ids2) {
16239
16239
  const idMap = {};
16240
- for (const doc of ids) {
16240
+ for (const doc of ids2) {
16241
16241
  idMap[doc.index] = doc._id;
16242
16242
  }
16243
16243
  return idMap;
@@ -50314,7 +50314,7 @@ init_cjs_shims();
50314
50314
 
50315
50315
  // ../../packages/core/src/version.ts
50316
50316
  init_cjs_shims();
50317
- var HORUS_VERSION = "0.1.2";
50317
+ var HORUS_VERSION = true ? "0.1.3" : "dev";
50318
50318
  var PINNED_AXON_VERSION = "1.0.1";
50319
50319
  var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
50320
50320
 
@@ -54530,7 +54530,9 @@ var repositorySchema = external_exports.object({
54530
54530
  });
54531
54531
  var connectorsSchema = external_exports.object({
54532
54532
  elasticsearch: external_exports.object({
54533
- indexPattern: external_exports.string(),
54533
+ indexPattern: external_exports.string().optional(),
54534
+ /** Multiple index patterns — joined with ',' when resolving. Takes precedence over indexPattern. */
54535
+ indexPatterns: external_exports.array(external_exports.string()).optional(),
54534
54536
  serviceName: external_exports.string().optional(),
54535
54537
  /**
54536
54538
  * Log schema preset. Controls which Elasticsearch field names Horus
@@ -54585,6 +54587,8 @@ var connectorsSchema = external_exports.object({
54585
54587
  }).optional(),
54586
54588
  grafana: external_exports.object({
54587
54589
  dashboard: external_exports.string().optional(),
54590
+ /** Multiple dashboard UIDs to fetch. Takes precedence over `dashboard` when set. */
54591
+ dashboards: external_exports.array(external_exports.string()).optional(),
54588
54592
  /** Direct URL value (takes priority over urlEnv). */
54589
54593
  url: external_exports.string().optional(),
54590
54594
  /** Name of the env var holding the Grafana base URL. Defaults to "GRAFANA_URL". */
@@ -54713,11 +54717,13 @@ function resolveEnvironment(config, opts) {
54713
54717
  if (c.elasticsearch !== void 0) {
54714
54718
  const es = c.elasticsearch;
54715
54719
  const url = es.url ?? process.env[es.urlEnv ?? "ES_URL"] ?? "";
54720
+ const effectivePattern = es.indexPatterns?.join(",") ?? es.indexPattern ?? "";
54716
54721
  resolved.elasticsearch = {
54717
54722
  url,
54718
54723
  username: es.username ?? process.env[es.usernameEnv ?? "ES_USERNAME"],
54719
54724
  password: es.password ?? process.env[es.passwordEnv ?? "ES_PASSWORD"],
54720
- indexPattern: es.indexPattern,
54725
+ indexPattern: effectivePattern,
54726
+ ...es.indexPatterns !== void 0 ? { indexPatterns: es.indexPatterns } : {},
54721
54727
  serviceName: es.serviceName,
54722
54728
  preset: es.preset,
54723
54729
  ...es.fields !== void 0 ? { fields: es.fields } : {}
@@ -54737,7 +54743,8 @@ function resolveEnvironment(config, opts) {
54737
54743
  url: g.url ?? process.env[g.urlEnv ?? "GRAFANA_URL"],
54738
54744
  username: g.username ?? process.env[g.usernameEnv ?? "GRAFANA_USER"],
54739
54745
  password: g.password ?? process.env[g.passwordEnv ?? "GRAFANA_PASSWORD"],
54740
- dashboard: g.dashboard
54746
+ dashboard: g.dashboard,
54747
+ ...g.dashboards !== void 0 ? { dashboards: g.dashboards } : {}
54741
54748
  };
54742
54749
  }
54743
54750
  if (c.redis !== void 0) {
@@ -54763,7 +54770,7 @@ var CONFIG_EXAMPLES = {
54763
54770
  "projects.*.repositories": 'e.g. [{ name: "my-api", path: "/path/to/repo" }]',
54764
54771
  "projects.*.repositories.*.name": 'e.g. name: "my-api"',
54765
54772
  "projects.*.repositories.*.path": 'e.g. path: "/absolute/path/to/repo"',
54766
- "projects.*.repositories.*.source.hostUrl": 'e.g. "http://127.0.0.1:8420" (start one with: axon host --port 8420)',
54773
+ "projects.*.repositories.*.source.hostUrl": 'e.g. "http://127.0.0.1:8420" (start one with: horus index)',
54767
54774
  "projects.*.repositories.*.axon.hostUrl": 'e.g. "http://127.0.0.1:8420" (deprecated: use source.hostUrl instead)',
54768
54775
  "projects.*.environments": 'e.g. [{ name: "production", connectors: {} }]',
54769
54776
  "projects.*.environments.*.name": 'e.g. name: "production"',
@@ -55046,17 +55053,21 @@ var import_node_fs3 = require("fs");
55046
55053
  var import_node_path3 = require("path");
55047
55054
  var import_node_net = require("net");
55048
55055
  var exec = (0, import_node_util.promisify)(import_node_child_process2.execFile);
55049
- async function axonAvailable() {
55056
+ var SOURCE_BINARY = "horus-source";
55057
+ async function resolveSourceBin() {
55050
55058
  try {
55051
- await exec("axon", ["--version"], { timeout: 5e3 });
55052
- return true;
55059
+ await exec(SOURCE_BINARY, ["--version"], { timeout: 5e3 });
55060
+ return SOURCE_BINARY;
55053
55061
  } catch {
55054
- return false;
55062
+ return null;
55055
55063
  }
55056
55064
  }
55065
+ async function axonAvailable() {
55066
+ return await resolveSourceBin() !== null;
55067
+ }
55057
55068
  async function getAxonVersion() {
55058
55069
  try {
55059
- const { stdout } = await exec("axon", ["--version"], { timeout: 5e3 });
55070
+ const { stdout } = await exec(SOURCE_BINARY, ["--version"], { timeout: 5e3 });
55060
55071
  const match = stdout.trim().match(/(\d+\.\d+\.\d+)/);
55061
55072
  return match?.[1] ?? null;
55062
55073
  } catch {
@@ -55064,10 +55075,12 @@ async function getAxonVersion() {
55064
55075
  }
55065
55076
  }
55066
55077
  function isAnalyzed(root) {
55067
- return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".axon"));
55078
+ return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".horus", "source")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".axon"));
55068
55079
  }
55069
55080
  async function analyzeRepo(root) {
55070
- await exec("axon", ["analyze", "."], {
55081
+ const bin = await resolveSourceBin();
55082
+ if (!bin) throw new Error("horus-source not found on PATH. Install it: pip install horus-source");
55083
+ await exec(bin, ["analyze", "."], {
55071
55084
  cwd: root,
55072
55085
  timeout: 9e5,
55073
55086
  maxBuffer: 64 * 1024 * 1024
@@ -55119,11 +55132,18 @@ function startHost(root, port) {
55119
55132
  (0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(root, ".horus"), { recursive: true });
55120
55133
  const logPath = (0, import_node_path3.join)(root, ".horus", "source-host.log");
55121
55134
  const fd = (0, import_node_fs3.openSync)(logPath, "a");
55122
- const child = (0, import_node_child_process2.spawn)("axon", ["host", "--port", String(port)], {
55135
+ const child = (0, import_node_child_process2.spawn)(SOURCE_BINARY, ["host", "--port", String(port)], {
55123
55136
  cwd: root,
55124
55137
  detached: true,
55125
55138
  stdio: ["ignore", fd, fd]
55126
55139
  });
55140
+ child.on("error", (err) => {
55141
+ if (err.code === "ENOENT") {
55142
+ process.stderr.write(
55143
+ "\nhorus-source not found on PATH. Install it: pip install horus-source\n"
55144
+ );
55145
+ }
55146
+ });
55127
55147
  if (child.pid !== void 0) {
55128
55148
  const record = {
55129
55149
  pid: child.pid,
@@ -55202,12 +55222,37 @@ var AxonCodeProvider = class {
55202
55222
  const h = await this.client.health();
55203
55223
  return {
55204
55224
  ok: h.ok,
55205
- detail: h.ok ? "Axon host responded " + h.status : "Axon host unreachable"
55225
+ detail: h.ok ? "Source intelligence host responded " + h.status : "Source intelligence host unreachable"
55206
55226
  };
55207
55227
  }
55208
55228
  async searchSymbols(query, limit = 10) {
55209
- const res = await this.client.search(query, limit);
55210
- return res.map((r) => ({ id: r.nodeId, name: r.name, filePath: r.filePath }));
55229
+ const E = this.escapeId(query);
55230
+ const exactQuery = `MATCH (n) WHERE toLower(n.name) = toLower("${E}") AND NOT n:File RETURN n.id, n.name, n.file_path LIMIT ${limit}`;
55231
+ const [exactRows, semanticRes] = await Promise.all([
55232
+ this.rows(exactQuery).catch(() => []),
55233
+ this.client.search(query, limit)
55234
+ ]);
55235
+ const exactSymbols = exactRows.map((row) => ({
55236
+ id: String(row[0] ?? ""),
55237
+ name: String(row[1] ?? ""),
55238
+ filePath: String(row[2] ?? "")
55239
+ }));
55240
+ const exactIds = new Set(exactSymbols.map((s) => s.id));
55241
+ const ql = query.toLowerCase();
55242
+ const semantic = semanticRes.filter((r) => !exactIds.has(r.nodeId)).map((r) => {
55243
+ const nl = r.name.toLowerCase();
55244
+ const isFile = r.label === "File";
55245
+ let rank = 0;
55246
+ if (!isFile && (nl.includes(ql) || ql.includes(nl))) rank = 2;
55247
+ else if (isFile && (nl.includes(ql) || ql.includes(nl))) rank = 1;
55248
+ return { r, rank };
55249
+ });
55250
+ semantic.sort((a, b2) => b2.rank - a.rank || b2.r.score - a.r.score);
55251
+ const combined = [
55252
+ ...exactSymbols,
55253
+ ...semantic.map(({ r }) => ({ id: r.nodeId, name: r.name, filePath: r.filePath }))
55254
+ ];
55255
+ return combined.slice(0, limit);
55211
55256
  }
55212
55257
  async context(symbolId) {
55213
55258
  const E = this.escapeId(symbolId);
@@ -55406,6 +55451,61 @@ var ElasticsearchClient = class {
55406
55451
  return { ok: false, detail: err.message };
55407
55452
  }
55408
55453
  }
55454
+ /**
55455
+ * Discover available index names. Resolution order:
55456
+ * 1. Data streams (modern ILM — clean names without date suffixes)
55457
+ * 2. Aliases (user-defined names mapping to one or more indices)
55458
+ * 3. Raw concrete indices (fallback for legacy clusters)
55459
+ * System entries (starting with '.') are always filtered out.
55460
+ * Returns [] on any error so callers can gracefully fall back.
55461
+ */
55462
+ async listIndices(signal) {
55463
+ const results = /* @__PURE__ */ new Set();
55464
+ try {
55465
+ const dsRes = await this.request("GET", "/_data_stream", void 0, signal);
55466
+ const streams = dsRes["data_streams"];
55467
+ if (Array.isArray(streams)) {
55468
+ for (const s of streams) {
55469
+ const name = String(s["name"] ?? "");
55470
+ if (name && !name.startsWith(".")) results.add(name);
55471
+ }
55472
+ }
55473
+ } catch {
55474
+ }
55475
+ try {
55476
+ const aliasRes = await this.request(
55477
+ "GET",
55478
+ "/_cat/aliases?format=json&h=alias&s=alias",
55479
+ void 0,
55480
+ signal
55481
+ );
55482
+ if (Array.isArray(aliasRes)) {
55483
+ for (const a of aliasRes) {
55484
+ const alias = String(a["alias"] ?? "");
55485
+ if (alias && !alias.startsWith(".")) results.add(alias);
55486
+ }
55487
+ }
55488
+ } catch {
55489
+ }
55490
+ if (results.size === 0) {
55491
+ try {
55492
+ const idxRes = await this.request(
55493
+ "GET",
55494
+ "/_cat/indices?format=json&h=index&s=index&expand_wildcards=open",
55495
+ void 0,
55496
+ signal
55497
+ );
55498
+ if (Array.isArray(idxRes)) {
55499
+ for (const r of idxRes) {
55500
+ const name = String(r["index"] ?? "");
55501
+ if (name && !name.startsWith(".")) results.add(name);
55502
+ }
55503
+ }
55504
+ } catch {
55505
+ }
55506
+ }
55507
+ return [...results].sort();
55508
+ }
55409
55509
  };
55410
55510
 
55411
55511
  // ../../packages/connectors/src/elasticsearch/provider.ts
@@ -56380,13 +56480,34 @@ function sanitizeExpr(expr) {
56380
56480
  if (replaced.includes("$")) return null;
56381
56481
  return replaced;
56382
56482
  }
56483
+ function extractHintTokens(hint) {
56484
+ const camelSplit = hint.replace(/([a-z])([A-Z])/g, "$1 $2");
56485
+ const acronymSplit = camelSplit.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
56486
+ return acronymSplit.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
56487
+ }
56383
56488
  function panelMatchesHint(p, hint) {
56384
56489
  if (hint === "") return true;
56385
- const tokens = hint.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
56490
+ const tokens = extractHintTokens(hint);
56386
56491
  if (tokens.length === 0) return true;
56387
56492
  const haystack = p.title.toLowerCase() + " " + p.exprs.map((e) => e.toLowerCase()).join(" ");
56388
56493
  return tokens.some((tok) => haystack.includes(tok));
56389
56494
  }
56495
+ function findMatchSource(p, hint) {
56496
+ if (hint === "") return null;
56497
+ const tokens = extractHintTokens(hint);
56498
+ if (tokens.length === 0) return null;
56499
+ if (tokens.some((tok) => p.title.toLowerCase().includes(tok))) return "panel-title";
56500
+ if (tokens.some((tok) => p.exprs.some((e) => e.toLowerCase().includes(tok)))) {
56501
+ return "query-text";
56502
+ }
56503
+ return null;
56504
+ }
56505
+ function findingLabelsMatchHint(labels, hint) {
56506
+ const tokens = extractHintTokens(hint);
56507
+ if (tokens.length === 0) return true;
56508
+ const haystack = Object.values(labels).join(" ").toLowerCase();
56509
+ return tokens.some((tok) => haystack.includes(tok));
56510
+ }
56390
56511
 
56391
56512
  // ../../packages/connectors/src/grafana/analyze.ts
56392
56513
  init_cjs_shims();
@@ -56498,7 +56619,8 @@ var GrafanaMetricsProvider = class {
56498
56619
  * Each panel is tagged with the dashboardUid it came from.
56499
56620
  */
56500
56621
  async findPanels(hint, signal) {
56501
- const dashboards = await this.client.searchDashboards(void 0, signal);
56622
+ const uids = this.opts.dashboardUids;
56623
+ const dashboards = uids !== void 0 && uids.length > 0 ? uids.map((uid2) => ({ uid: uid2, title: uid2 })) : await this.client.searchDashboards(void 0, signal);
56502
56624
  const allPanels = [];
56503
56625
  for (const dash of dashboards) {
56504
56626
  signal?.throwIfAborted();
@@ -56521,48 +56643,63 @@ var GrafanaMetricsProvider = class {
56521
56643
  /**
56522
56644
  * Discover relevant panels, query current + baseline windows for each expr,
56523
56645
  * and return MetricFindings (including "none" anomalies for completeness).
56646
+ *
56647
+ * Two-pass hint matching:
56648
+ * 1. Filter panels by title / PromQL expression (fast, cheap).
56649
+ * 2. If no panels match on pass 1, query ALL panels and filter the returned
56650
+ * series by their label values (source, topic, queue, etc.).
56524
56651
  */
56525
56652
  async analyze(opts) {
56526
56653
  const { signal } = opts;
56527
56654
  const step = opts.step ?? this.opts.defaultStep;
56528
56655
  const windowSecs = opts.to - opts.from;
56529
- const panels = await this.findPanels(opts.hint, signal);
56530
- const allFindings = [];
56531
- for (const panel of panels) {
56532
- signal?.throwIfAborted();
56533
- for (const rawExpr of panel.exprs) {
56656
+ const hint = opts.hint;
56657
+ const queryPanels = async (panels) => {
56658
+ const results = [];
56659
+ for (const panel of panels) {
56534
56660
  signal?.throwIfAborted();
56535
- const expr = sanitizeExpr(rawExpr);
56536
- if (expr === null) continue;
56537
- try {
56538
- const [currentResp, baselineResp] = await Promise.all([
56539
- this.client.datasourceRange(panel.datasourceUid, expr, opts.from, opts.to, step, signal),
56540
- this.client.datasourceRange(
56541
- panel.datasourceUid,
56542
- expr,
56543
- opts.from - windowSecs,
56544
- opts.from,
56545
- step,
56546
- signal
56547
- )
56548
- ]);
56549
- const current = parseRange(currentResp);
56550
- const baseline = parseRange(baselineResp);
56551
- const findings2 = buildFindings(
56552
- panel.dashboardUid ?? "",
56553
- panel.title,
56554
- panel.kind,
56555
- baseline,
56556
- current
56557
- );
56558
- allFindings.push(...findings2);
56559
- } catch (err) {
56560
- if (signal?.aborted) throw signal.reason ?? err;
56561
- continue;
56661
+ const panelMatchSrc = hint !== void 0 && hint !== "" ? findMatchSource(panel, hint) : null;
56662
+ for (const rawExpr of panel.exprs) {
56663
+ signal?.throwIfAborted();
56664
+ const expr = sanitizeExpr(rawExpr);
56665
+ if (expr === null) continue;
56666
+ try {
56667
+ const [currentResp, baselineResp] = await Promise.all([
56668
+ this.client.datasourceRange(panel.datasourceUid, expr, opts.from, opts.to, step, signal),
56669
+ this.client.datasourceRange(
56670
+ panel.datasourceUid,
56671
+ expr,
56672
+ opts.from - windowSecs,
56673
+ opts.from,
56674
+ step,
56675
+ signal
56676
+ )
56677
+ ]);
56678
+ const current = parseRange(currentResp);
56679
+ const baseline = parseRange(baselineResp);
56680
+ const findings2 = buildFindings(
56681
+ panel.dashboardUid ?? "",
56682
+ panel.title,
56683
+ panel.kind,
56684
+ baseline,
56685
+ current
56686
+ ).map((f) => ({ ...f, matchSource: panelMatchSrc }));
56687
+ results.push(...findings2);
56688
+ } catch (err) {
56689
+ if (signal?.aborted) throw signal.reason ?? err;
56690
+ continue;
56691
+ }
56562
56692
  }
56563
56693
  }
56694
+ return results;
56695
+ };
56696
+ const matchedPanels = await this.findPanels(hint, signal);
56697
+ if (matchedPanels.length > 0 || hint === void 0 || hint === "") {
56698
+ return queryPanels(matchedPanels);
56564
56699
  }
56565
- return allFindings;
56700
+ const allPanels = await this.findPanels(void 0, signal);
56701
+ const allFindings = await queryPanels(allPanels);
56702
+ return allFindings.filter((f) => findingLabelsMatchHint(f.labels, hint)).map((f) => ({ ...f, matchSource: "series-labels" }));
56566
56703
  }
56567
56704
  /**
56568
56705
  * Raw escape hatch: execute a single PromQL expression against the default
@@ -56603,12 +56740,19 @@ var MongoStateClient = class {
56603
56740
  opts;
56604
56741
  client = null;
56605
56742
  assertAllowed(collection) {
56743
+ if (this.opts.allowlist.length === 0) return;
56606
56744
  if (!this.opts.allowlist.includes(collection)) {
56607
56745
  throw new Error(
56608
56746
  `Collection "${collection}" is not allowlisted for ${this.opts.database}`
56609
56747
  );
56610
56748
  }
56611
56749
  }
56750
+ /** List all collection names in the configured database (for auto-discovery). */
56751
+ async listCollections() {
56752
+ const db = await this.db();
56753
+ const cols = await db.listCollections({}, { nameOnly: true }).toArray();
56754
+ return cols.map((c) => c["name"]).filter(Boolean);
56755
+ }
56612
56756
  async db() {
56613
56757
  if (this.client === null) {
56614
56758
  this.client = new import_mongodb.MongoClient(this.opts.url, {
@@ -56806,7 +56950,13 @@ var MongoStateProvider = class {
56806
56950
  const legacyHours = opts.legacyHours ?? DEFAULT_LEGACY_HOURS;
56807
56951
  const nowMs = Date.now();
56808
56952
  const collections = [];
56809
- for (const coll of this.opts.collections) {
56953
+ let targetCollections = this.opts.collections;
56954
+ let autoDiscovered = false;
56955
+ if (targetCollections.length === 0) {
56956
+ targetCollections = await this.client.listCollections();
56957
+ autoDiscovered = true;
56958
+ }
56959
+ for (const coll of targetCollections) {
56810
56960
  try {
56811
56961
  const count = await this.client.count(coll);
56812
56962
  const fields = await this.client.sampleFields(coll);
@@ -56838,7 +56988,13 @@ var MongoStateProvider = class {
56838
56988
  } catch {
56839
56989
  }
56840
56990
  }
56841
- return { database: this.opts.database, staleHours, legacyHours, collections };
56991
+ return {
56992
+ database: this.opts.database,
56993
+ staleHours,
56994
+ legacyHours,
56995
+ collections,
56996
+ autoDiscovered
56997
+ };
56842
56998
  }
56843
56999
  toEvidence(analysis) {
56844
57000
  return stateToEvidence(analysis, "mongo.analyzeState", (/* @__PURE__ */ new Date()).toISOString());
@@ -56846,6 +57002,9 @@ var MongoStateProvider = class {
56846
57002
  async health() {
56847
57003
  return this.client.health();
56848
57004
  }
57005
+ async listCollections() {
57006
+ return this.client.listCollections();
57007
+ }
56849
57008
  async close() {
56850
57009
  await this.client.close();
56851
57010
  }
@@ -57206,7 +57365,7 @@ function metricsForEnv(renv) {
57206
57365
  if (!g || !g.url) return null;
57207
57366
  return new GrafanaMetricsProvider(
57208
57367
  new GrafanaClient({ baseUrl: g.url, username: g.username, password: g.password }),
57209
- { defaultStep: 60 }
57368
+ { defaultStep: 60, dashboardUids: g.dashboards }
57210
57369
  );
57211
57370
  }
57212
57371
  function queueForEnv(renv) {
@@ -57328,23 +57487,28 @@ function parseFileContributors(stdout) {
57328
57487
  for (const line2 of stdout.split("\n")) {
57329
57488
  const trimmed = line2.trim();
57330
57489
  if (!trimmed) continue;
57331
- const sepIdx = trimmed.indexOf("");
57332
- if (sepIdx === -1) continue;
57333
- const author = trimmed.slice(0, sepIdx).trim();
57334
- const date2 = trimmed.slice(sepIdx + 1).trim();
57335
- if (!author || !date2) continue;
57336
- const existing = tally.get(author);
57490
+ const parts = trimmed.split("");
57491
+ if (parts.length < 3) continue;
57492
+ const name = parts[0].trim();
57493
+ const email = parts[1].trim();
57494
+ const date2 = parts[2].trim();
57495
+ if (!name || !date2) continue;
57496
+ const key = email || name;
57497
+ const existing = tally.get(key);
57337
57498
  if (existing === void 0) {
57338
- tally.set(author, { commits: 1, firstDate: date2, lastDate: date2 });
57499
+ tally.set(key, { name, commits: 1, firstDate: date2, lastDate: date2 });
57339
57500
  } else {
57340
57501
  existing.commits += 1;
57341
57502
  if (date2 < existing.firstDate) existing.firstDate = date2;
57342
- if (date2 > existing.lastDate) existing.lastDate = date2;
57503
+ if (date2 > existing.lastDate) {
57504
+ existing.lastDate = date2;
57505
+ existing.name = name;
57506
+ }
57343
57507
  }
57344
57508
  }
57345
57509
  const result = [];
57346
- for (const [author, stats] of tally.entries()) {
57347
- result.push({ author, ...stats });
57510
+ for (const [, stats] of tally.entries()) {
57511
+ result.push({ author: stats.name, commits: stats.commits, firstDate: stats.firstDate, lastDate: stats.lastDate });
57348
57512
  }
57349
57513
  result.sort((a, b2) => {
57350
57514
  if (b2.commits !== a.commits) return b2.commits - a.commits;
@@ -57356,7 +57520,7 @@ async function gitFileContributors(repoPath, file) {
57356
57520
  try {
57357
57521
  const { stdout } = await exec2(
57358
57522
  "git",
57359
- ["-C", repoPath, "log", "--follow", "--format=%an%x1f%aI", "--", file],
57523
+ ["-C", repoPath, "log", "--follow", "--format=%an%x1f%ae%x1f%aI", "--", file],
57360
57524
  { maxBuffer: 10 * 1024 * 1024 }
57361
57525
  );
57362
57526
  return parseFileContributors(stdout);
@@ -66366,6 +66530,9 @@ async function listQueueEdges(db, opts = {}) {
66366
66530
 
66367
66531
  // ../../packages/db/src/investigations.ts
66368
66532
  init_cjs_shims();
66533
+ async function updateInvestigationReport(db, id, report) {
66534
+ await db.update(investigations).set({ report }).where(eq(investigations.id, id));
66535
+ }
66369
66536
  async function getInvestigation(db, id) {
66370
66537
  const rows = await db.select().from(investigations).where(eq(investigations.id, id)).limit(1);
66371
66538
  return rows[0] ?? null;
@@ -66511,12 +66678,14 @@ function mark(ok) {
66511
66678
  if (ok === "pending") return import_picocolors.default.yellow("\u25CB");
66512
66679
  return ok ? import_picocolors.default.green("\u25CF") : import_picocolors.default.red("\u25CF");
66513
66680
  }
66514
- async function checkEnv(renv) {
66681
+ async function checkEnv(renv, deps) {
66515
66682
  const header = ` ${import_picocolors.default.bold(renv.project)} / ${import_picocolors.default.bold(renv.env)}` + (renv.readOnly ? import_picocolors.default.dim(" (read-only)") : "");
66516
66683
  console.log(header);
66517
66684
  let allOk = true;
66518
66685
  if (renv.repositories.length === 0) {
66519
- console.log(` ${mark("pending")} ${import_picocolors.default.bold("Source")} ${import_picocolors.default.dim("no repositories configured")}`);
66686
+ console.log(
66687
+ ` ${mark("pending")} ${import_picocolors.default.bold("Source")} ${import_picocolors.default.dim("no repositories configured")}`
66688
+ );
66520
66689
  }
66521
66690
  for (const repo of renv.repositories) {
66522
66691
  const axonHostUrl = repo.sourceHostUrl ?? repo.axonHostUrl;
@@ -66540,7 +66709,9 @@ async function checkEnv(renv) {
66540
66709
  versionPart = `v${compat.version} (pinned ${compat.pinned} \u2014 MISMATCH)`;
66541
66710
  }
66542
66711
  const axonDetail = health.ok ? `${repo.name} \xB7 responded ${health.status} \xB7 ${versionPart} at ${axonHostUrl}` : `${repo.name} \xB7 unreachable at ${axonHostUrl}`;
66543
- console.log(` ${mark(health.ok)} ${import_picocolors.default.bold("Source")} ${import_picocolors.default.dim(axonDetail)}`);
66712
+ console.log(
66713
+ ` ${mark(health.ok)} ${import_picocolors.default.bold("Source")} ${import_picocolors.default.dim(axonDetail)}`
66714
+ );
66544
66715
  if (!health.ok) allOk = false;
66545
66716
  }
66546
66717
  const esCfg = renv.connectors.elasticsearch;
@@ -66548,12 +66719,14 @@ async function checkEnv(renv) {
66548
66719
  const logsProvider = logsForEnv(renv);
66549
66720
  if (logsProvider) {
66550
66721
  const h = await logsProvider.health();
66551
- const detail = h.ok ? `reachable \xB7 index ${esCfg.indexPattern}` : `unreachable \xB7 index ${esCfg.indexPattern}`;
66722
+ const idxDisplay = esCfg.indexPatterns ? esCfg.indexPatterns.join(", ") : esCfg.indexPattern;
66723
+ const detail = h.ok ? `reachable \xB7 index ${idxDisplay}` : `unreachable \xB7 index ${idxDisplay}`;
66552
66724
  console.log(` ${mark(h.ok)} ${import_picocolors.default.bold("Elasticsearch")} ${import_picocolors.default.dim(detail)}`);
66553
66725
  if (!h.ok) allOk = false;
66554
66726
  } else {
66727
+ const idxDisplay = esCfg.indexPatterns ? esCfg.indexPatterns.join(", ") : esCfg.indexPattern;
66555
66728
  console.log(
66556
- ` ${mark(false)} ${import_picocolors.default.bold("Elasticsearch")} ${import_picocolors.default.dim(`configured (index ${esCfg.indexPattern}) but ES_URL not set`)}`
66729
+ ` ${mark(false)} ${import_picocolors.default.bold("Elasticsearch")} ${import_picocolors.default.dim(`configured (index ${idxDisplay}) but ES_URL not set`)}`
66557
66730
  );
66558
66731
  }
66559
66732
  } else {
@@ -66566,7 +66739,8 @@ async function checkEnv(renv) {
66566
66739
  const metricsProvider = metricsForEnv(renv);
66567
66740
  if (metricsProvider) {
66568
66741
  const h = await metricsProvider.health();
66569
- const dashSuffix = grafanaCfg.dashboard ? ` \xB7 dashboard ${grafanaCfg.dashboard}` : "";
66742
+ const dashDisplay = grafanaCfg.dashboards ? grafanaCfg.dashboards.join(", ") : grafanaCfg.dashboard;
66743
+ const dashSuffix = dashDisplay ? ` \xB7 dashboards: ${dashDisplay}` : "";
66570
66744
  const detail = h.ok ? `reachable${dashSuffix}` : `unreachable${dashSuffix}`;
66571
66745
  console.log(` ${mark(h.ok)} ${import_picocolors.default.bold("Grafana")} ${import_picocolors.default.dim(detail)}`);
66572
66746
  if (!h.ok) allOk = false;
@@ -66582,20 +66756,73 @@ async function checkEnv(renv) {
66582
66756
  }
66583
66757
  const mongoCfg = renv.connectors.mongodb;
66584
66758
  if (mongoCfg) {
66585
- const collCount = mongoCfg.collections.length;
66586
- const detail = `configured: db ${mongoCfg.database}, ${collCount} collection(s)` + import_picocolors.default.dim(" (provider: HOR-33)");
66587
- console.log(` ${mark("pending")} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim(detail)}`);
66759
+ const mongo = (deps?.mongoFactory ?? mongoForEnv)(renv);
66760
+ if (mongo) {
66761
+ try {
66762
+ const h = await mongo.health();
66763
+ if (!h.ok) {
66764
+ console.log(
66765
+ ` ${mark(false)} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim(`unreachable \xB7 db ${mongoCfg.database}`)}`
66766
+ );
66767
+ allOk = false;
66768
+ } else {
66769
+ const allowlist = mongoCfg.collections;
66770
+ const allowlistPart = allowlist.length === 0 ? "allowlist: all" : `allowlist: ${allowlist.length}`;
66771
+ const discovered = mongo.listCollections ? await mongo.listCollections() : void 0;
66772
+ const discoveredPart = discovered ? ` \xB7 discovered: ${discovered.length} collection(s)` : "";
66773
+ const detail = `reachable \xB7 db ${mongoCfg.database} \xB7 ${allowlistPart}${discoveredPart}`;
66774
+ console.log(
66775
+ ` ${mark(true)} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim(detail)}`
66776
+ );
66777
+ }
66778
+ } finally {
66779
+ await mongo.close();
66780
+ }
66781
+ } else {
66782
+ console.log(
66783
+ ` ${mark(false)} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim(`configured (db ${mongoCfg.database}) but Mongo URL not set`)}`
66784
+ );
66785
+ }
66786
+ } else {
66787
+ console.log(
66788
+ ` ${mark("pending")} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim("not configured")}`
66789
+ );
66790
+ }
66791
+ const redisCfg = renv.connectors.redis;
66792
+ if (redisCfg?.url) {
66793
+ const safeUrl = redactRedisUrl(redisCfg.url);
66794
+ console.log(
66795
+ ` ${mark("pending")} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`configured \xB7 ${safeUrl}`)}`
66796
+ );
66588
66797
  } else {
66589
- console.log(` ${mark("pending")} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim("not configured")}`);
66798
+ console.log(
66799
+ ` ${mark("pending")} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim("not configured")}`
66800
+ );
66590
66801
  }
66591
66802
  console.log("");
66592
66803
  return allOk;
66593
66804
  }
66805
+ function redactRedisUrl(raw) {
66806
+ try {
66807
+ const u = new URL(raw);
66808
+ if (u.password) {
66809
+ u.password = "***";
66810
+ }
66811
+ if (u.username) {
66812
+ u.username = u.username === "" ? "" : "***";
66813
+ }
66814
+ return u.toString();
66815
+ } catch {
66816
+ return raw.replace(/\/\/:?[^@]*@/, "//:***@");
66817
+ }
66818
+ }
66594
66819
  async function runStatus(configPath, opts) {
66595
66820
  console.log(import_picocolors.default.bold(`
66596
66821
  Horus ${HORUS_VERSION}`));
66597
- console.log(import_picocolors.default.dim(`pinned backend: ${PINNED_SOURCE_VERSION} \xB7 transport: HTTP/MCP only
66598
- `));
66822
+ console.log(
66823
+ import_picocolors.default.dim(`pinned backend: ${PINNED_SOURCE_VERSION} \xB7 transport: HTTP/MCP only
66824
+ `)
66825
+ );
66599
66826
  let config;
66600
66827
  const checks = [];
66601
66828
  try {
@@ -66618,8 +66845,12 @@ Horus ${HORUS_VERSION}`));
66618
66845
  }
66619
66846
  const dbUrl = config.database.url;
66620
66847
  const h = await checkDatabase(dbUrl);
66621
- console.log(` ${mark(h.reachable)} ${import_picocolors.default.bold("Postgres")} ${import_picocolors.default.dim(h.reachableDetail)}`);
66622
- console.log(` ${mark(h.schemaReady)} ${import_picocolors.default.bold("Schema")} ${import_picocolors.default.dim(h.schemaDetail)}`);
66848
+ console.log(
66849
+ ` ${mark(h.reachable)} ${import_picocolors.default.bold("Postgres")} ${import_picocolors.default.dim(h.reachableDetail)}`
66850
+ );
66851
+ console.log(
66852
+ ` ${mark(h.schemaReady)} ${import_picocolors.default.bold("Schema")} ${import_picocolors.default.dim(h.schemaDetail)}`
66853
+ );
66623
66854
  console.log("");
66624
66855
  const envList = listEnvironments(config);
66625
66856
  if (envList.length === 0) {
@@ -66634,7 +66865,7 @@ Horus ${HORUS_VERSION}`));
66634
66865
  console.error(import_picocolors.default.red(err.message));
66635
66866
  return 1;
66636
66867
  }
66637
- const ok = await checkEnv(renv);
66868
+ const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory });
66638
66869
  return ok ? 0 : 1;
66639
66870
  }
66640
66871
  let allHealthy = true;
@@ -66647,7 +66878,7 @@ Horus ${HORUS_VERSION}`));
66647
66878
  allHealthy = false;
66648
66879
  continue;
66649
66880
  }
66650
- const ok = await checkEnv(renv);
66881
+ const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory });
66651
66882
  if (!ok) allHealthy = false;
66652
66883
  }
66653
66884
  return checks.some((c) => c.ok === false && c.fatal) ? 1 : 0;
@@ -66666,11 +66897,32 @@ async function runExplain(query, opts) {
66666
66897
  }
66667
66898
  const symbols = await code.searchSymbols(query, 5);
66668
66899
  if (symbols.length === 0) {
66669
- console.log("No symbol found for: " + query);
66900
+ if (await isQueueBoundary(config, query)) {
66901
+ console.log(`No symbol found for: ${import_picocolors2.default.bold(query)}`);
66902
+ console.log(
66903
+ import_picocolors2.default.dim(` "${query}" matches an async boundary \u2014 try: `) + import_picocolors2.default.bold(`horus queues ${query}`)
66904
+ );
66905
+ return 1;
66906
+ }
66907
+ console.log(`No symbol found for: ${query}`);
66908
+ console.log(import_picocolors2.default.dim(` Tip: use an exact class or function name, e.g. "MyService" or "processOrder"`));
66670
66909
  return 1;
66671
66910
  }
66672
66911
  const top = symbols[0];
66673
66912
  if (!top) return 1;
66913
+ const isExactMatch = top.name.toLowerCase() === query.toLowerCase();
66914
+ if (!isExactMatch) {
66915
+ if (await isQueueBoundary(config, query)) {
66916
+ console.log(`No exact symbol match for: ${import_picocolors2.default.bold(query)}`);
66917
+ console.log(
66918
+ import_picocolors2.default.dim(` "${query}" matches an async boundary \u2014 try: `) + import_picocolors2.default.bold(`horus queues ${query}`)
66919
+ );
66920
+ return 1;
66921
+ }
66922
+ console.log(
66923
+ import_picocolors2.default.yellow(` No exact match for "${query}"`) + import_picocolors2.default.dim(` \u2014 showing closest: "${top.name}" (fuzzy match)`)
66924
+ );
66925
+ }
66674
66926
  const siblings = symbols.filter((s) => s.name === top.name && s.id !== top.id);
66675
66927
  const [ctx, impact, flows] = await Promise.all([
66676
66928
  code.context(top.id),
@@ -66698,6 +66950,20 @@ async function runExplain(query, opts) {
66698
66950
  renderReport(top, ctx, impact, flows, siblings);
66699
66951
  return 0;
66700
66952
  }
66953
+ async function isQueueBoundary(config, query) {
66954
+ try {
66955
+ const { db, sql: sql2 } = createDb(config.database.url);
66956
+ try {
66957
+ const edges = await listQueueEdges(db, { queueName: query });
66958
+ return edges.length > 0;
66959
+ } finally {
66960
+ await sql2.end().catch(() => {
66961
+ });
66962
+ }
66963
+ } catch {
66964
+ return false;
66965
+ }
66966
+ }
66701
66967
  function renderReport(top, ctx, impact, flows, siblings) {
66702
66968
  const kind = top.id.includes(":") ? top.id.substring(0, top.id.indexOf(":")) : top.id;
66703
66969
  const sym = ctx.symbol;
@@ -66798,22 +67064,22 @@ function extractQueueGraph(input) {
66798
67064
  }
66799
67065
  }
66800
67066
  const producers = [];
66801
- for (const pc33 of input.producerClasses) {
66802
- for (const m of pc33.content.matchAll(INJECT_QUEUE_RE)) {
67067
+ for (const pc34 of input.producerClasses) {
67068
+ for (const m of pc34.content.matchAll(INJECT_QUEUE_RE)) {
66803
67069
  const queue = m[1] ?? "";
66804
- if (queue) producers.push({ queue, symbol: pc33.name, file: pc33.filePath });
67070
+ if (queue) producers.push({ queue, symbol: pc34.name, file: pc34.filePath });
66805
67071
  }
66806
- for (const m of pc33.content.matchAll(NEW_BULL_RE)) {
67072
+ for (const m of pc34.content.matchAll(NEW_BULL_RE)) {
66807
67073
  if (m[2] !== "Queue") continue;
66808
67074
  const queue = resolveArg(m[3] ?? "", constMap);
66809
67075
  if (queue) {
66810
- producers.push({ queue, symbol: (m[1] ?? pc33.name) || baseName(pc33.filePath), file: pc33.filePath });
67076
+ producers.push({ queue, symbol: (m[1] ?? pc34.name) || baseName(pc34.filePath), file: pc34.filePath });
66811
67077
  }
66812
67078
  }
66813
- for (const m of pc33.content.matchAll(QUEUE_NAME_CONST_RE)) {
67079
+ for (const m of pc34.content.matchAll(QUEUE_NAME_CONST_RE)) {
66814
67080
  const ident = m[1] ?? "";
66815
67081
  const queue = m[2] ?? "";
66816
- if (queue) producers.push({ queue, symbol: ident || baseName(pc33.filePath), file: pc33.filePath });
67082
+ if (queue) producers.push({ queue, symbol: ident || baseName(pc34.filePath), file: pc34.filePath });
66817
67083
  }
66818
67084
  }
66819
67085
  const workers = [];
@@ -66995,7 +67261,7 @@ async function runIndex(opts) {
66995
67261
  spawned = true;
66996
67262
  console.log(import_picocolors3.default.bold(`Indexing ${label}`) + import_picocolors3.default.dim(` (${root})`));
66997
67263
  if (!await sourceAvailable()) {
66998
- console.error(import_picocolors3.default.red("Source-intelligence backend not found on PATH. Install it (see `horus setup`) and retry."));
67264
+ console.error(import_picocolors3.default.red("horus-source not found on PATH. Install it: pip install horus-source"));
66999
67265
  return 1;
67000
67266
  }
67001
67267
  if (!isAnalyzed(root)) {
@@ -67073,64 +67339,29 @@ init_cjs_shims();
67073
67339
  var import_picocolors4 = __toESM(require_picocolors(), 1);
67074
67340
  async function runQueues(name, opts) {
67075
67341
  try {
67076
- const config = await loadConfig(opts.config);
67342
+ const config = await loadConfig(opts.config, { name: opts.name });
67077
67343
  const { db, sql: sql2 } = createDb(config.database.url);
67078
67344
  try {
67079
67345
  const rows = await listQueueEdges(db, { project: opts.project, queueName: name });
67346
+ console.log(
67347
+ import_picocolors4.default.bold("Queue topology") + import_picocolors4.default.dim(" \xB7 source: code / source intelligence \xB7 static (run horus index to refresh)")
67348
+ );
67349
+ console.log("");
67080
67350
  if (rows.length === 0) {
67081
- console.log("No queue edges. Run: horus index");
67082
- return 0;
67351
+ console.log(import_picocolors4.default.dim(" No queue edges indexed. Run: horus index"));
67352
+ } else {
67353
+ const byQueue = buildQueueMap(rows);
67354
+ printTopology(byQueue);
67083
67355
  }
67084
- const byQueue = /* @__PURE__ */ new Map();
67085
- for (const row of rows) {
67086
- const existing = byQueue.get(row.queueName);
67087
- if (existing) {
67088
- existing.push(row);
67089
- } else {
67090
- byQueue.set(row.queueName, [row]);
67091
- }
67092
- }
67093
- for (const [queueName, edges] of byQueue) {
67094
- console.log(import_picocolors4.default.bold(queueName));
67095
- const producerSet = /* @__PURE__ */ new Set();
67096
- const producerDetails = /* @__PURE__ */ new Map();
67097
- for (const edge of edges) {
67098
- if (edge.producerSymbol) {
67099
- producerSet.add(edge.producerSymbol);
67100
- if (edge.producerFile) {
67101
- producerDetails.set(edge.producerSymbol, edge.producerFile);
67102
- }
67103
- }
67104
- }
67105
- if (producerSet.size === 0) {
67106
- console.log(" producers: " + import_picocolors4.default.dim("none"));
67107
- } else {
67108
- const producerList = Array.from(producerSet).map((sym) => {
67109
- const file = producerDetails.get(sym);
67110
- return file ? `${sym} (${file})` : sym;
67111
- }).join(", ");
67112
- console.log(" producers: " + producerList);
67113
- }
67114
- const workerSet = /* @__PURE__ */ new Set();
67115
- const workerDetails = /* @__PURE__ */ new Map();
67116
- for (const edge of edges) {
67117
- if (edge.workerSymbol) {
67118
- workerSet.add(edge.workerSymbol);
67119
- if (edge.workerFile) {
67120
- workerDetails.set(edge.workerSymbol, edge.workerFile);
67121
- }
67122
- }
67123
- }
67124
- if (workerSet.size === 0) {
67125
- console.log(" workers: " + import_picocolors4.default.dim("none"));
67126
- } else {
67127
- const workerList = Array.from(workerSet).map((sym) => {
67128
- const file = workerDetails.get(sym);
67129
- return file ? `${sym} (${file})` : sym;
67130
- }).join(", ");
67131
- console.log(" workers: " + workerList);
67132
- }
67133
- console.log("");
67356
+ console.log("");
67357
+ if (opts.live) {
67358
+ await runLiveMode(config, rows, name);
67359
+ } else {
67360
+ console.log(
67361
+ import_picocolors4.default.dim(
67362
+ " Tip: run horus queues --live to show real-time Redis/BullMQ depths and failed-job counts."
67363
+ )
67364
+ );
67134
67365
  }
67135
67366
  } finally {
67136
67367
  await sql2.end();
@@ -67141,6 +67372,141 @@ async function runQueues(name, opts) {
67141
67372
  return 1;
67142
67373
  }
67143
67374
  }
67375
+ function buildQueueMap(rows) {
67376
+ const byQueue = /* @__PURE__ */ new Map();
67377
+ for (const row of rows) {
67378
+ const existing = byQueue.get(row.queueName);
67379
+ if (existing) {
67380
+ existing.push(row);
67381
+ } else {
67382
+ byQueue.set(row.queueName, [row]);
67383
+ }
67384
+ }
67385
+ return byQueue;
67386
+ }
67387
+ function printTopology(byQueue) {
67388
+ for (const [queueName, edges] of byQueue) {
67389
+ console.log(import_picocolors4.default.bold(queueName));
67390
+ const producerSet = /* @__PURE__ */ new Set();
67391
+ const producerDetails = /* @__PURE__ */ new Map();
67392
+ for (const edge of edges) {
67393
+ if (edge.producerSymbol) {
67394
+ producerSet.add(edge.producerSymbol);
67395
+ if (edge.producerFile) producerDetails.set(edge.producerSymbol, edge.producerFile);
67396
+ }
67397
+ }
67398
+ if (producerSet.size === 0) {
67399
+ console.log(" producers: " + import_picocolors4.default.dim("none"));
67400
+ } else {
67401
+ const list = Array.from(producerSet).map((sym) => {
67402
+ const file = producerDetails.get(sym);
67403
+ return file ? `${sym} (${file})` : sym;
67404
+ }).join(", ");
67405
+ console.log(" producers: " + list);
67406
+ }
67407
+ const workerSet = /* @__PURE__ */ new Set();
67408
+ const workerDetails = /* @__PURE__ */ new Map();
67409
+ for (const edge of edges) {
67410
+ if (edge.workerSymbol) {
67411
+ workerSet.add(edge.workerSymbol);
67412
+ if (edge.workerFile) workerDetails.set(edge.workerSymbol, edge.workerFile);
67413
+ }
67414
+ }
67415
+ if (workerSet.size === 0) {
67416
+ console.log(" workers: " + import_picocolors4.default.dim("none"));
67417
+ } else {
67418
+ const list = Array.from(workerSet).map((sym) => {
67419
+ const file = workerDetails.get(sym);
67420
+ return file ? `${sym} (${file})` : sym;
67421
+ }).join(", ");
67422
+ console.log(" workers: " + list);
67423
+ }
67424
+ console.log("");
67425
+ }
67426
+ }
67427
+ async function runLiveMode(config, rows, nameFilter) {
67428
+ let renv;
67429
+ try {
67430
+ renv = resolveEnvironment(config);
67431
+ } catch (err) {
67432
+ console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
67433
+ console.log(import_picocolors4.default.yellow(` \u26A0 Cannot resolve environment: ${err.message}`));
67434
+ return;
67435
+ }
67436
+ const queueProvider = queueForEnv(renv);
67437
+ if (!queueProvider) {
67438
+ console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
67439
+ console.log(
67440
+ import_picocolors4.default.yellow(" \u25CB Redis not configured \u2014 run: ") + import_picocolors4.default.bold("horus connect redis")
67441
+ );
67442
+ return;
67443
+ }
67444
+ let headerPrinted = false;
67445
+ try {
67446
+ const health = await queueProvider.health();
67447
+ if (!health.ok) {
67448
+ console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
67449
+ console.log(import_picocolors4.default.red(` \u2717 Redis unreachable: ${health.detail}`));
67450
+ return;
67451
+ }
67452
+ const topologyNames = [...buildQueueMap(rows).keys()];
67453
+ const queueNames = nameFilter !== void 0 ? [nameFilter] : topologyNames.length > 0 ? topologyNames : void 0;
67454
+ const state = await queueProvider.analyzeQueues({ queueNames });
67455
+ const collectedAt = new Date(state.collectedAt).toLocaleTimeString();
67456
+ console.log(
67457
+ import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(` \xB7 source: Redis/BullMQ (prefix: ${state.prefix}) \xB7 collected: ${collectedAt}`)
67458
+ );
67459
+ headerPrinted = true;
67460
+ console.log("");
67461
+ if (state.queues.length === 0) {
67462
+ console.log(import_picocolors4.default.dim(" No queues found in Redis."));
67463
+ console.log(
67464
+ import_picocolors4.default.dim(
67465
+ " If queues exist under a custom prefix, set the BullMQ prefix in your connector config."
67466
+ )
67467
+ );
67468
+ return;
67469
+ }
67470
+ printLiveTable(state.queues);
67471
+ } catch (err) {
67472
+ if (!headerPrinted) {
67473
+ console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
67474
+ }
67475
+ console.log(import_picocolors4.default.red(` \u2717 ${err.message}`));
67476
+ } finally {
67477
+ await queueProvider.close().catch(() => {
67478
+ });
67479
+ }
67480
+ }
67481
+ function printLiveTable(queues) {
67482
+ const nameWidth = Math.max(10, ...queues.map((q) => q.queueName.length));
67483
+ const numWidth = 7;
67484
+ const header = " " + "queue".padEnd(nameWidth) + " " + "waiting".padStart(numWidth) + " " + "active".padStart(numWidth) + " " + "failed".padStart(numWidth) + " " + "delayed".padStart(numWidth) + " " + "paused".padStart(numWidth);
67485
+ console.log(import_picocolors4.default.dim(header));
67486
+ console.log(import_picocolors4.default.dim(" " + "\u2500".repeat(header.length - 2)));
67487
+ for (const q of queues) {
67488
+ const hasIssue = q.failed > 0 || q.waiting > 100 || q.delayed > 50 || q.isPaused;
67489
+ const color = hasIssue ? import_picocolors4.default.yellow : (s) => s;
67490
+ const row = " " + q.queueName.padEnd(nameWidth) + " " + String(q.waiting).padStart(numWidth) + " " + String(q.active).padStart(numWidth) + " " + String(q.failed).padStart(numWidth) + " " + String(q.delayed).padStart(numWidth) + " " + (q.isPaused ? import_picocolors4.default.yellow("paused") : String(q.paused).padStart(numWidth));
67491
+ console.log(color(row));
67492
+ if (q.oldestWaitingMs !== void 0) {
67493
+ const age = formatAge(q.oldestWaitingMs);
67494
+ console.log(import_picocolors4.default.dim(` oldest waiting: ${age}`));
67495
+ }
67496
+ if (q.failedBreakdown && q.failedBreakdown.length > 0) {
67497
+ for (const { reason, count } of q.failedBreakdown.slice(0, 3)) {
67498
+ console.log(import_picocolors4.default.red(` \u2717 [${count}x] ${reason}`));
67499
+ }
67500
+ }
67501
+ }
67502
+ console.log("");
67503
+ }
67504
+ function formatAge(ms) {
67505
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
67506
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
67507
+ if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
67508
+ return `${Math.round(ms / 864e5)}d`;
67509
+ }
67144
67510
 
67145
67511
  // ../../packages/cli/src/commands/investigate.ts
67146
67512
  init_cjs_shims();
@@ -67157,8 +67523,96 @@ init_cjs_shims();
67157
67523
 
67158
67524
  // ../../packages/engine/src/ownership.ts
67159
67525
  init_cjs_shims();
67526
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
67527
+ "ts",
67528
+ "tsx",
67529
+ "js",
67530
+ "jsx",
67531
+ "mjs",
67532
+ "cjs",
67533
+ "py",
67534
+ "rb",
67535
+ "go",
67536
+ "rs",
67537
+ "java",
67538
+ "kt",
67539
+ "swift",
67540
+ "cs",
67541
+ "cpp",
67542
+ "c",
67543
+ "h",
67544
+ "hpp",
67545
+ "cc",
67546
+ "vue",
67547
+ "svelte",
67548
+ "astro",
67549
+ "php",
67550
+ "scala",
67551
+ "clj",
67552
+ "ex",
67553
+ "exs",
67554
+ "hs",
67555
+ "sh",
67556
+ "bash",
67557
+ "zsh",
67558
+ "json",
67559
+ "yaml",
67560
+ "yml",
67561
+ "toml",
67562
+ "sql",
67563
+ "graphql",
67564
+ "proto",
67565
+ "html",
67566
+ "css",
67567
+ "scss",
67568
+ "sass",
67569
+ "less",
67570
+ "md",
67571
+ "mdx"
67572
+ ]);
67573
+ function looksLikeFilePath(query) {
67574
+ const base2 = query.split("/").pop() ?? "";
67575
+ const dotIdx = base2.lastIndexOf(".");
67576
+ if (dotIdx <= 0) return false;
67577
+ return CODE_EXTENSIONS.has(base2.slice(dotIdx + 1).toLowerCase());
67578
+ }
67160
67579
  async function estimateOwnership(query, deps) {
67161
- const top = deps.symbol ?? (await deps.code.searchSymbols(query, 5))[0] ?? null;
67580
+ let top = deps.symbol ?? null;
67581
+ const needsSearch = deps.symbol === void 0 || deps.symbol === null;
67582
+ if (needsSearch) {
67583
+ if (looksLikeFilePath(query)) {
67584
+ const queryBase = query.split("/").pop() ?? query;
67585
+ const broad = await deps.code.searchSymbols(query, 20);
67586
+ const byFile = /* @__PURE__ */ new Map();
67587
+ for (const sym of broad) {
67588
+ const fp = sym.filePath;
67589
+ if (fp === query || fp.endsWith("/" + query) || fp.split("/").pop() === queryBase) {
67590
+ if (!byFile.has(fp)) byFile.set(fp, sym);
67591
+ }
67592
+ }
67593
+ if (byFile.size === 1) {
67594
+ top = [...byFile.values()][0] ?? null;
67595
+ } else if (byFile.size > 1) {
67596
+ return {
67597
+ query,
67598
+ symbol: null,
67599
+ file: null,
67600
+ contributors: [],
67601
+ likelyMaintainer: null,
67602
+ maintainerShare: 0,
67603
+ mostActiveRecent: null,
67604
+ confidence: 0,
67605
+ evidence: [],
67606
+ candidates: [...byFile.keys()],
67607
+ note: "Ambiguous: " + byFile.size + ' files match "' + query + '". Use a more specific path to disambiguate.'
67608
+ };
67609
+ } else {
67610
+ top = (await deps.code.searchSymbols(query, 5))[0] ?? null;
67611
+ }
67612
+ } else {
67613
+ top = (await deps.code.searchSymbols(query, 5))[0] ?? null;
67614
+ }
67615
+ }
67162
67616
  const file = top?.filePath ?? null;
67163
67617
  if (file === null) {
67164
67618
  return {
@@ -67282,7 +67736,48 @@ function processEvidence(ev, nodes, edges) {
67282
67736
  addEdge(edges, "observed_in", evId, deployId, ev.id);
67283
67737
  return;
67284
67738
  }
67285
- // symbol, flow, impact, redis-key, metric: evidence node only, no infra derived
67739
+ case "symbol": {
67740
+ const p = ev.payload;
67741
+ const sym = p.symbol ?? {};
67742
+ const name = sym.name ?? "";
67743
+ if (!name) return;
67744
+ const symbolId = `symbol:${name}`;
67745
+ upsertNode(nodes, symbolId, "symbol", name, ev.id);
67746
+ addEdge(edges, "observed_in", evId, symbolId, ev.id);
67747
+ const filePath = sym.filePath ?? p.filePath ?? ev.links.file;
67748
+ if (filePath) {
67749
+ const fileId = `file:${filePath}`;
67750
+ upsertNode(nodes, fileId, "file", filePath, ev.id);
67751
+ addEdge(edges, "in-file", symbolId, fileId, ev.id);
67752
+ }
67753
+ return;
67754
+ }
67755
+ case "flow": {
67756
+ const p = ev.payload;
67757
+ const name = p.name ?? ev.title;
67758
+ const flowNodeId = `flow:${p.flowId ?? name}`;
67759
+ upsertNode(nodes, flowNodeId, "flow", name, ev.id);
67760
+ addEdge(edges, "observed_in", evId, flowNodeId, ev.id);
67761
+ for (const stepName of p.steps ?? []) {
67762
+ if (!stepName) continue;
67763
+ const stepSymbolId = `symbol:${stepName}`;
67764
+ upsertNode(nodes, stepSymbolId, "symbol", stepName);
67765
+ addEdge(edges, "symbol-in-flow", stepSymbolId, flowNodeId, ev.id);
67766
+ }
67767
+ return;
67768
+ }
67769
+ case "metric": {
67770
+ const p = ev.payload;
67771
+ const labels = p.labels ?? {};
67772
+ const svc = labels["service"] ?? labels["job"] ?? labels["instance"];
67773
+ if (svc) {
67774
+ const serviceId = `service:${svc}`;
67775
+ upsertNode(nodes, serviceId, "service", svc, ev.id);
67776
+ addEdge(edges, "metric-from", evId, serviceId, ev.id);
67777
+ }
67778
+ return;
67779
+ }
67780
+ // impact, redis-key: evidence node only, no infra derived
67286
67781
  default:
67287
67782
  return;
67288
67783
  }
@@ -67303,18 +67798,28 @@ function scoreImplication(nodes, edges, evidence2) {
67303
67798
  const evById = new Map(evidence2.map((e) => [e.id, e]));
67304
67799
  for (const node of nodes.values()) {
67305
67800
  if (node.type === "evidence") continue;
67801
+ if (node.type === "symbol" || node.type === "file" || node.type === "flow") continue;
67306
67802
  node.implicationScore = node.evidenceIds.reduce((max, eid) => {
67307
67803
  const ev = evById.get(eid);
67308
67804
  if (!ev || isExcludedFromImplication(ev)) return max;
67309
67805
  return Math.max(max, ev.relevance);
67310
67806
  }, 0);
67311
67807
  }
67808
+ const CODE_TOPOLOGY_NODE_TYPES = /* @__PURE__ */ new Set([
67809
+ "evidence",
67810
+ "symbol",
67811
+ "file",
67812
+ "flow"
67813
+ ]);
67312
67814
  const adj = /* @__PURE__ */ new Map();
67313
67815
  for (const edge of edges.values()) {
67314
67816
  if (edge.type === "observed_in") continue;
67817
+ if (edge.type === "in-file") continue;
67818
+ if (edge.type === "symbol-in-flow") continue;
67819
+ if (edge.type === "metric-from") continue;
67315
67820
  const f = nodes.get(edge.from);
67316
67821
  const t = nodes.get(edge.to);
67317
- if (!f || !t || f.type === "evidence" || t.type === "evidence") continue;
67822
+ if (!f || !t || CODE_TOPOLOGY_NODE_TYPES.has(f.type) || CODE_TOPOLOGY_NODE_TYPES.has(t.type)) continue;
67318
67823
  const fs3 = adj.get(edge.from) ?? /* @__PURE__ */ new Set();
67319
67824
  const ts = adj.get(edge.to) ?? /* @__PURE__ */ new Set();
67320
67825
  fs3.add(edge.to);
@@ -67339,6 +67844,7 @@ function scoreImplication(nodes, edges, evidence2) {
67339
67844
  }
67340
67845
  for (const node of nodes.values()) {
67341
67846
  if (node.type === "evidence") continue;
67847
+ if (node.type === "symbol" || node.type === "file" || node.type === "flow") continue;
67342
67848
  node.implicated = node.implicationScore >= IMPLICATION_THRESHOLD;
67343
67849
  }
67344
67850
  }
@@ -67370,6 +67876,302 @@ function implicatedNodeIds(graph, evidenceIds) {
67370
67876
  (n) => n.type !== "evidence" && n.implicated && n.evidenceIds.some((eid) => idSet.has(eid))
67371
67877
  ).map((n) => n.id);
67372
67878
  }
67879
+ function implicatedEvidenceIdsByNodeType(graph, nodeType) {
67880
+ const ids2 = [];
67881
+ for (const node of graph.nodes) {
67882
+ if (node.type !== nodeType || !node.implicated) continue;
67883
+ for (const eid of node.evidenceIds) {
67884
+ if (!ids2.includes(eid)) ids2.push(eid);
67885
+ }
67886
+ }
67887
+ return ids2;
67888
+ }
67889
+
67890
+ // ../../packages/engine/src/cause-chain.ts
67891
+ init_cjs_shims();
67892
+ function groupByKind(ids2, evById) {
67893
+ const map2 = /* @__PURE__ */ new Map();
67894
+ for (const id of ids2) {
67895
+ const ev = evById.get(id);
67896
+ if (!ev) continue;
67897
+ const list = map2.get(ev.kind) ?? [];
67898
+ list.push(ev);
67899
+ map2.set(ev.kind, list);
67900
+ }
67901
+ return map2;
67902
+ }
67903
+ function ids(evs) {
67904
+ return evs.map((e) => e.id);
67905
+ }
67906
+ function firstImplicatedNode(graph, type) {
67907
+ return graph.nodes.find((n) => n.type === type && n.implicated)?.id;
67908
+ }
67909
+ function chainForDeploymentRegression(hyp, byKind, graph, seedLabel) {
67910
+ const commitEvs = byKind.get("commit") ?? [];
67911
+ const symbolEvs = byKind.get("symbol") ?? [];
67912
+ const logEvs = byKind.get("log") ?? [];
67913
+ const metricEvs = byKind.get("metric") ?? [];
67914
+ const impactEvs = byKind.get("impact") ?? [];
67915
+ const runtimeEvIds = [...ids(logEvs), ...ids(metricEvs)];
67916
+ const steps = [];
67917
+ steps.push({
67918
+ role: "trigger",
67919
+ label: commitEvs.length > 0 ? `Recent change: ${commitEvs[0]?.title ?? "commit"}` : "Recent change (commit range not captured)",
67920
+ evidenceIds: ids(commitEvs),
67921
+ graphNodeId: graph.nodes.find((n) => n.type === "deployment")?.id
67922
+ });
67923
+ steps.push({
67924
+ role: "propagation",
67925
+ label: `Affected code path: ${seedLabel}` + (impactEvs.length > 0 ? ` (${impactEvs[0]?.payload?.affected ?? "?"} downstream symbol(s))` : ""),
67926
+ evidenceIds: [...ids(symbolEvs), ...ids(impactEvs)],
67927
+ graphNodeId: graph.nodes.find((n) => n.type === "symbol")?.id
67928
+ });
67929
+ if (runtimeEvIds.length > 0) {
67930
+ steps.push({
67931
+ role: "symptom",
67932
+ label: logEvs.length > 0 ? `Runtime error signatures observed (${logEvs.length} signature(s))` : `Metric anomalies observed (${metricEvs.length} signal(s))`,
67933
+ evidenceIds: runtimeEvIds,
67934
+ graphNodeId: firstImplicatedNode(graph, "service")
67935
+ });
67936
+ }
67937
+ const nonEmpty = steps.filter((s) => s.evidenceIds.length > 0 || s.role === "trigger");
67938
+ const summary = `Change to ${seedLabel} \u2192 affected code path` + (runtimeEvIds.length > 0 ? " \u2192 runtime symptoms observed" : "");
67939
+ return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps: nonEmpty, summary };
67940
+ }
67941
+ function chainForQueueBacklog(hyp, byKind, graph, queueName) {
67942
+ const queueEdgeEvs = byKind.get("queue-edge") ?? [];
67943
+ const queueStateEvs = byKind.get("queue-state") ?? [];
67944
+ const logEvs = byKind.get("log") ?? [];
67945
+ const metricEvs = byKind.get("metric") ?? [];
67946
+ const queueNodeId = queueName ? graph.nodes.find((n) => n.id === `queue:${queueName}`)?.id : firstImplicatedNode(graph, "queue");
67947
+ const steps = [];
67948
+ steps.push({
67949
+ role: "trigger",
67950
+ label: queueName ? `Producer enqueuing to "${queueName}" faster than consumer drains` : "Producer enqueuing faster than consumer drains",
67951
+ evidenceIds: ids(queueEdgeEvs),
67952
+ graphNodeId: queueNodeId
67953
+ });
67954
+ if (queueStateEvs.length > 0) {
67955
+ steps.push({
67956
+ role: "propagation",
67957
+ label: `Queue depth growing${queueName ? ` on "${queueName}"` : ""}`,
67958
+ evidenceIds: ids(queueStateEvs),
67959
+ graphNodeId: queueNodeId
67960
+ });
67961
+ }
67962
+ const symptomEvIds = [...ids(logEvs), ...ids(metricEvs)];
67963
+ if (symptomEvIds.length > 0) {
67964
+ steps.push({
67965
+ role: "symptom",
67966
+ label: "Worker throughput degraded \u2014 downstream delays observable",
67967
+ evidenceIds: symptomEvIds,
67968
+ graphNodeId: firstImplicatedNode(graph, "worker") ?? firstImplicatedNode(graph, "service")
67969
+ });
67970
+ }
67971
+ const summary = (queueName ? `"${queueName}" queue` : "Queue") + " backed up \u2014 producers outpacing consumers" + (symptomEvIds.length > 0 ? " \u2192 downstream delays" : "");
67972
+ return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
67973
+ }
67974
+ function chainForWorkerSlowdown(hyp, byKind, graph, queueName) {
67975
+ const queueStateEvs = byKind.get("queue-state") ?? [];
67976
+ const metricEvs = byKind.get("metric") ?? [];
67977
+ const logEvs = byKind.get("log") ?? [];
67978
+ const workerNodeId = firstImplicatedNode(graph, "worker");
67979
+ const queueNodeId = queueName ? graph.nodes.find((n) => n.id === `queue:${queueName}`)?.id : firstImplicatedNode(graph, "queue");
67980
+ const steps = [];
67981
+ const triggerEvIds = [...ids(queueStateEvs.filter((e) => {
67982
+ const p = e.payload;
67983
+ return p?.isPaused === true || p?.active === 0;
67984
+ })), ...ids(metricEvs)];
67985
+ steps.push({
67986
+ role: "trigger",
67987
+ label: queueName ? `Workers consuming "${queueName}" stalled or processing slowly` : "Workers stalled or processing slowly",
67988
+ evidenceIds: triggerEvIds.length > 0 ? triggerEvIds : ids(queueStateEvs),
67989
+ graphNodeId: workerNodeId
67990
+ });
67991
+ if (queueStateEvs.length > 0) {
67992
+ steps.push({
67993
+ role: "propagation",
67994
+ label: `Queue depth accumulating${queueName ? ` on "${queueName}"` : ""}`,
67995
+ evidenceIds: ids(queueStateEvs),
67996
+ graphNodeId: queueNodeId
67997
+ });
67998
+ }
67999
+ if (logEvs.length > 0) {
68000
+ steps.push({
68001
+ role: "symptom",
68002
+ label: "Downstream latency and error signatures observed",
68003
+ evidenceIds: ids(logEvs),
68004
+ graphNodeId: firstImplicatedNode(graph, "service")
68005
+ });
68006
+ }
68007
+ const summary = "Worker stall on " + (queueName ? `"${queueName}"` : "queue") + " \u2192 queue depth growing" + (logEvs.length > 0 ? " \u2192 downstream errors" : "");
68008
+ return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
68009
+ }
68010
+ function chainForExternalApiLatency(hyp, byKind, graph) {
68011
+ const metricEvs = byKind.get("metric") ?? [];
68012
+ const logEvs = byKind.get("log") ?? [];
68013
+ const queueStateEvs = byKind.get("queue-state") ?? [];
68014
+ const steps = [];
68015
+ steps.push({
68016
+ role: "trigger",
68017
+ label: `External or upstream dependency latency spike (${metricEvs.length} metric signal(s))`,
68018
+ evidenceIds: ids(metricEvs),
68019
+ graphNodeId: firstImplicatedNode(graph, "service")
68020
+ });
68021
+ if (logEvs.length > 0) {
68022
+ steps.push({
68023
+ role: "propagation",
68024
+ label: "Calls to upstream timing out or returning errors",
68025
+ evidenceIds: ids(logEvs),
68026
+ graphNodeId: firstImplicatedNode(graph, "service")
68027
+ });
68028
+ }
68029
+ if (queueStateEvs.length > 0) {
68030
+ steps.push({
68031
+ role: "symptom",
68032
+ label: "Queue backlog accumulating from stalled downstream workers",
68033
+ evidenceIds: ids(queueStateEvs),
68034
+ graphNodeId: firstImplicatedNode(graph, "queue")
68035
+ });
68036
+ }
68037
+ const summary = "External API latency spike" + (logEvs.length > 0 ? " \u2192 timeout errors propagating" : "") + (queueStateEvs.length > 0 ? " \u2192 queue backlog" : "");
68038
+ return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
68039
+ }
68040
+ function chainForInfrastructure(hyp, byKind, graph) {
68041
+ const stateEvs = byKind.get("state") ?? [];
68042
+ const queueStateEvs = byKind.get("queue-state") ?? [];
68043
+ const logEvs = byKind.get("log") ?? [];
68044
+ const metricEvs = byKind.get("metric") ?? [];
68045
+ const triggerEvIds = [...ids(stateEvs), ...ids(metricEvs)];
68046
+ const propagationEvIds = [...ids(queueStateEvs)];
68047
+ const symptomEvIds = ids(logEvs);
68048
+ const steps = [];
68049
+ steps.push({
68050
+ role: "trigger",
68051
+ label: "Infrastructure component degraded (database, network, or cache)",
68052
+ evidenceIds: triggerEvIds.length > 0 ? triggerEvIds : ids(queueStateEvs),
68053
+ graphNodeId: firstImplicatedNode(graph, "collection") ?? firstImplicatedNode(graph, "database") ?? firstImplicatedNode(graph, "queue")
68054
+ });
68055
+ if (propagationEvIds.length > 0) {
68056
+ steps.push({
68057
+ role: "propagation",
68058
+ label: "Dependent services / workers lost access to the degraded component",
68059
+ evidenceIds: propagationEvIds,
68060
+ graphNodeId: firstImplicatedNode(graph, "worker") ?? firstImplicatedNode(graph, "service")
68061
+ });
68062
+ }
68063
+ if (symptomEvIds.length > 0) {
68064
+ steps.push({
68065
+ role: "symptom",
68066
+ label: "Processing failures and error signatures observed",
68067
+ evidenceIds: symptomEvIds,
68068
+ graphNodeId: firstImplicatedNode(graph, "service")
68069
+ });
68070
+ }
68071
+ const summary = "Infrastructure degradation \u2192 services/workers affected" + (symptomEvIds.length > 0 ? " \u2192 processing failures" : "");
68072
+ return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
68073
+ }
68074
+ function chainForRetryStorm(hyp, byKind, graph) {
68075
+ const logEvs = byKind.get("log") ?? [];
68076
+ const queueStateEvs = byKind.get("queue-state") ?? [];
68077
+ const metricEvs = byKind.get("metric") ?? [];
68078
+ const steps = [];
68079
+ steps.push({
68080
+ role: "trigger",
68081
+ label: `Initial failure with automatic retry enabled (${logEvs.length} log signature(s) spiking)`,
68082
+ evidenceIds: ids(logEvs),
68083
+ graphNodeId: firstImplicatedNode(graph, "service")
68084
+ });
68085
+ if (queueStateEvs.length > 0) {
68086
+ steps.push({
68087
+ role: "propagation",
68088
+ label: "Retry amplification growing queue depth and error volume",
68089
+ evidenceIds: ids(queueStateEvs),
68090
+ graphNodeId: firstImplicatedNode(graph, "queue")
68091
+ });
68092
+ }
68093
+ if (metricEvs.length > 0) {
68094
+ steps.push({
68095
+ role: "symptom",
68096
+ label: "Cascading load visible in metric anomalies",
68097
+ evidenceIds: ids(metricEvs),
68098
+ graphNodeId: firstImplicatedNode(graph, "service")
68099
+ });
68100
+ }
68101
+ const summary = "Initial failure + retry \u2192 amplified load" + (metricEvs.length > 0 ? " \u2192 cascading metric anomalies" : "");
68102
+ return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
68103
+ }
68104
+ function chainGeneric(hyp, byKind, graph) {
68105
+ const allEvIds = hyp.supportingEvidenceIds;
68106
+ const logOrMetric = [
68107
+ ...byKind.get("log") ?? [],
68108
+ ...byKind.get("metric") ?? []
68109
+ ];
68110
+ const steps = [
68111
+ {
68112
+ role: "trigger",
68113
+ label: hyp.statement,
68114
+ evidenceIds: allEvIds,
68115
+ graphNodeId: firstImplicatedNode(graph, "service") ?? firstImplicatedNode(graph, "queue")
68116
+ }
68117
+ ];
68118
+ if (logOrMetric.length > 0) {
68119
+ steps.push({
68120
+ role: "symptom",
68121
+ label: "Observable symptoms in logs / metrics",
68122
+ evidenceIds: ids(logOrMetric),
68123
+ graphNodeId: firstImplicatedNode(graph, "service")
68124
+ });
68125
+ }
68126
+ return {
68127
+ hypothesisId: hyp.id,
68128
+ category: hyp.category,
68129
+ confidence: hyp.confidence,
68130
+ steps,
68131
+ summary: hyp.statement
68132
+ };
68133
+ }
68134
+ function buildCauseChains(hypotheses2, evidence2, graph, seedLabel) {
68135
+ const evById = new Map(evidence2.map((e) => [e.id, e]));
68136
+ const chains = [];
68137
+ for (const hyp of hypotheses2) {
68138
+ if (hyp.verdict !== "supported" && hyp.verdict !== "weakened") continue;
68139
+ const byKind = groupByKind(hyp.supportingEvidenceIds, evById);
68140
+ let chain;
68141
+ switch (hyp.category) {
68142
+ case "deployment-regression":
68143
+ chain = chainForDeploymentRegression(hyp, byKind, graph, seedLabel);
68144
+ break;
68145
+ case "queue-backlog": {
68146
+ const queueName = extractQueueName(hyp.statement);
68147
+ chain = chainForQueueBacklog(hyp, byKind, graph, queueName);
68148
+ break;
68149
+ }
68150
+ case "worker-slowdown": {
68151
+ const queueName = extractQueueName(hyp.statement);
68152
+ chain = chainForWorkerSlowdown(hyp, byKind, graph, queueName);
68153
+ break;
68154
+ }
68155
+ case "external-api-latency":
68156
+ chain = chainForExternalApiLatency(hyp, byKind, graph);
68157
+ break;
68158
+ case "infrastructure":
68159
+ chain = chainForInfrastructure(hyp, byKind, graph);
68160
+ break;
68161
+ case "retry-storm":
68162
+ chain = chainForRetryStorm(hyp, byKind, graph);
68163
+ break;
68164
+ default:
68165
+ chain = chainGeneric(hyp, byKind, graph);
68166
+ }
68167
+ chains.push(chain);
68168
+ }
68169
+ return chains;
68170
+ }
68171
+ function extractQueueName(statement) {
68172
+ const m = /\bon ([a-zA-Z0-9_-]+)/.exec(statement) ?? /consuming ([a-zA-Z0-9_-]+)/.exec(statement);
68173
+ return m?.[1];
68174
+ }
67373
68175
 
67374
68176
  // ../../packages/engine/src/score-cause.ts
67375
68177
  init_cjs_shims();
@@ -67632,6 +68434,15 @@ function generateHypotheses(evidence2, _correlation, ctx) {
67632
68434
  const queueEvs = evidence2.filter((e) => e.kind === "queue-edge");
67633
68435
  const hasCommit = commitEvs.length > 0;
67634
68436
  const { queues } = ctx;
68437
+ const logSpikeEvIds = evidence2.filter((e) => e.kind === "log" && e.ratio !== void 0 && e.ratio >= 2).map((e) => e.id);
68438
+ const stateEvIds = evidence2.filter((e) => e.kind === "state").map((e) => e.id);
68439
+ const allQueueBacklogEvIds = [
68440
+ ...ctx.queueBacklogEvIdsByQueue?.values() ?? []
68441
+ ].flat();
68442
+ const allQueueStarvationEvIds = [
68443
+ ...ctx.queueStarvationEvIdsByQueue?.values() ?? []
68444
+ ].flat();
68445
+ const graphImplicatedCollectionEvIds = ctx.graph ? implicatedEvidenceIdsByNodeType(ctx.graph, "collection") : [];
67635
68446
  const hyps = [];
67636
68447
  hyps.push({
67637
68448
  id: globalThis.crypto.randomUUID(),
@@ -67640,9 +68451,7 @@ function generateHypotheses(evidence2, _correlation, ctx) {
67640
68451
  confidence: hasCommit ? 0.5 : 0.15,
67641
68452
  supportingEvidenceIds: commitEvs.map((e) => e.id),
67642
68453
  contradictingEvidenceIds: [],
67643
- missingEvidence: hasCommit ? [] : [
67644
- "A change/deployment range \u2014 re-run with --since <ref> to diff what shipped"
67645
- ]
68454
+ missingEvidence: hasCommit ? [] : ctx.sinceProvided ? ["No git changes found in the specified range \u2014 verify the ref is accessible or use HEAD~N for exact commit ranges"] : ["A change/deployment range \u2014 re-run with --since <ref> to diff what shipped"]
67646
68455
  });
67647
68456
  for (const queueName of queues) {
67648
68457
  const backlogEvIds = ctx.queueBacklogEvIdsByQueue?.get(queueName) ?? [];
@@ -67678,25 +68487,32 @@ function generateHypotheses(evidence2, _correlation, ctx) {
67678
68487
  contradictingEvidenceIds: [],
67679
68488
  missingEvidence: latencyMetricEvIds.length > 0 ? [] : ["Request latency metrics (Grafana) + error logs (Elasticsearch)"]
67680
68489
  });
68490
+ const retryStormSupport = [.../* @__PURE__ */ new Set([...logSpikeEvIds, ...allQueueBacklogEvIds])];
67681
68491
  hyps.push({
67682
68492
  id: globalThis.crypto.randomUUID(),
67683
68493
  category: "retry-storm",
67684
68494
  statement: "A retry storm is amplifying load on the failing path.",
67685
- confidence: 0.15,
67686
- supportingEvidenceIds: [],
67687
- contradictingEvidenceIds: [],
67688
- missingEvidence: [
67689
- "Retry/error logs + queue retry statistics (Elasticsearch + BullMQ)"
67690
- ]
68495
+ confidence: retryStormSupport.length > 0 ? 0.35 : 0.15,
68496
+ supportingEvidenceIds: retryStormSupport,
68497
+ contradictingEvidenceIds: allQueueStarvationEvIds,
68498
+ missingEvidence: retryStormSupport.length > 0 ? [] : ["Retry/error logs + queue retry statistics (Elasticsearch + BullMQ)"]
67691
68499
  });
68500
+ const infraSupport = [
68501
+ .../* @__PURE__ */ new Set([
68502
+ ...stateEvIds,
68503
+ ...allQueueStarvationEvIds,
68504
+ ...ctx.latencyMetricEvIds ?? [],
68505
+ ...graphImplicatedCollectionEvIds
68506
+ ])
68507
+ ];
67692
68508
  hyps.push({
67693
68509
  id: globalThis.crypto.randomUUID(),
67694
68510
  category: "infrastructure",
67695
68511
  statement: "An infrastructure issue (database, Redis, or network) is degrading processing.",
67696
- confidence: 0.15,
67697
- supportingEvidenceIds: [],
68512
+ confidence: infraSupport.length > 0 ? 0.35 : 0.15,
68513
+ supportingEvidenceIds: infraSupport,
67698
68514
  contradictingEvidenceIds: [],
67699
- missingEvidence: ["Infra/Redis metrics (Grafana) + Redis state"]
68515
+ missingEvidence: infraSupport.length > 0 ? [] : ["Infra/Redis metrics (Grafana) + Redis state"]
67700
68516
  });
67701
68517
  hyps.sort((a, b2) => b2.confidence - a.confidence);
67702
68518
  return hyps;
@@ -67917,7 +68733,8 @@ function detectMissingEvidence(r, connectors = {}) {
67917
68733
  blindSpots.push("Cannot see the real error.");
67918
68734
  }
67919
68735
  if (!hasMetric && !(connectors.grafana && connectors.metricsCollected)) {
67920
- const metricsWhy = !connectors.grafana ? "No Grafana connector configured \u2014 cannot see latency/error-rate trends." : "Grafana metrics collection failed or timed out \u2014 metric trends unavailable for this investigation.";
68736
+ const failureDetail = connectors.metricsFailureReason ? ` (${connectors.metricsFailureReason})` : "";
68737
+ const metricsWhy = !connectors.grafana ? "No Grafana connector configured \u2014 cannot see latency/error-rate trends." : `Grafana metrics collection failed or timed out${failureDetail} \u2014 metric trends unavailable for this investigation.`;
67921
68738
  const metricsNextSource = !connectors.grafana ? "Add a `grafana` connector to the environment" : 'Check Grafana connectivity, then run `horus metrics "<hint>"` manually';
67922
68739
  gaps.push({
67923
68740
  dimension: "metrics",
@@ -67931,7 +68748,7 @@ function detectMissingEvidence(r, connectors = {}) {
67931
68748
  gaps.push({
67932
68749
  dimension: "queue runtime state",
67933
68750
  why: connectors.redis ? "Queue topology is known but live depth + failed/delayed counts were not collected." : "Queue topology is known but there is no Redis/BullMQ connector for live depth/failures.",
67934
- nextSource: connectors.redis ? "Inspect `horus queues`" : "Add a `redis` connector to read live BullMQ state",
68751
+ nextSource: connectors.redis ? "Run `horus queues --live` to see real-time queue depths and failed-job counts" : "Add a `redis` connector to read live BullMQ state",
67935
68752
  confidenceImpact: 0.1
67936
68753
  });
67937
68754
  blindSpots.push("Cannot determine if the queue is actually backed up.");
@@ -67939,8 +68756,8 @@ function detectMissingEvidence(r, connectors = {}) {
67939
68756
  if (!hasCommit) {
67940
68757
  gaps.push({
67941
68758
  dimension: "deployment records",
67942
- why: "No deployment/change data in scope \u2014 cannot tell what shipped before the incident.",
67943
- nextSource: "Re-run with --since <ref>, or `horus what-changed <service>`",
68759
+ why: connectors.sinceProvided ? "No git changes found in the specified range \u2014 the ref may not be diffable or no commits fall in this window." : "No deployment/change data in scope \u2014 cannot tell what shipped before the incident.",
68760
+ nextSource: connectors.sinceProvided ? "Use HEAD~N or a specific SHA/branch for git diff ranges (e.g. --since HEAD~5)" : "Re-run with --since <ref>, or `horus what-changed <service>`",
67944
68761
  confidenceImpact: 0.08
67945
68762
  });
67946
68763
  blindSpots.push("Cannot correlate with a recent change.");
@@ -68117,33 +68934,33 @@ function buildGroups(evidence2) {
68117
68934
  }
68118
68935
  }
68119
68936
  const raw = [];
68120
- for (const [key, ids] of symbolMap) {
68121
- if (ids.length >= 2) {
68937
+ for (const [key, ids2] of symbolMap) {
68938
+ if (ids2.length >= 2) {
68122
68939
  raw.push({
68123
68940
  key,
68124
68941
  dimension: "symbol",
68125
68942
  reason: `Share symbol ${key}`,
68126
- evidenceIds: [...ids]
68943
+ evidenceIds: [...ids2]
68127
68944
  });
68128
68945
  }
68129
68946
  }
68130
- for (const [key, ids] of fileMap) {
68131
- if (ids.length >= 2) {
68947
+ for (const [key, ids2] of fileMap) {
68948
+ if (ids2.length >= 2) {
68132
68949
  raw.push({
68133
68950
  key,
68134
68951
  dimension: "file",
68135
68952
  reason: `Share file ${key}`,
68136
- evidenceIds: [...ids]
68953
+ evidenceIds: [...ids2]
68137
68954
  });
68138
68955
  }
68139
68956
  }
68140
- for (const [key, ids] of queueMap) {
68141
- if (ids.length >= 2) {
68957
+ for (const [key, ids2] of queueMap) {
68958
+ if (ids2.length >= 2) {
68142
68959
  raw.push({
68143
68960
  key,
68144
68961
  dimension: "queue",
68145
68962
  reason: `Share queue ${key}`,
68146
- evidenceIds: [...ids]
68963
+ evidenceIds: [...ids2]
68147
68964
  });
68148
68965
  }
68149
68966
  }
@@ -68167,13 +68984,13 @@ function buildChains(evidence2) {
68167
68984
  const commitEvs = evidence2.filter((e) => e.kind === "commit");
68168
68985
  const symbolEvs = evidence2.filter((e) => e.kind === "symbol");
68169
68986
  const relevanceById = new Map(evidence2.map((e) => [e.id, e.relevance]));
68170
- function avgRelevance(ids) {
68171
- if (ids.length === 0) return 0;
68987
+ function avgRelevance(ids2) {
68988
+ if (ids2.length === 0) return 0;
68172
68989
  let sum = 0;
68173
- for (const id of ids) {
68990
+ for (const id of ids2) {
68174
68991
  sum += relevanceById.get(id) ?? 0;
68175
68992
  }
68176
- return sum / ids.length;
68993
+ return sum / ids2.length;
68177
68994
  }
68178
68995
  const chains = [];
68179
68996
  if (queueEdges2.length > 0) {
@@ -68277,7 +69094,7 @@ function seedRole(s) {
68277
69094
  if (/util|helper/i.test(hay)) return "util";
68278
69095
  return "code";
68279
69096
  }
68280
- function scoreSeed(s, index2) {
69097
+ function scoreSeed(s, index2, hintTokens) {
68281
69098
  const hay = `${s.name} ${s.filePath}`.toLowerCase();
68282
69099
  let score = 0;
68283
69100
  if (PREFER.test(hay)) score += 3;
@@ -68285,10 +69102,20 @@ function scoreSeed(s, index2) {
68285
69102
  if (/\.(resolver|controller|service)\.[jt]sx?$/i.test(s.filePath)) score += 2;
68286
69103
  if (/(^|\/)scripts?\//i.test(s.filePath)) score -= 2;
68287
69104
  score += Math.max(0, 5 - index2) * 0.1;
69105
+ if (hintTokens !== void 0 && hintTokens.length > 0) {
69106
+ let hintBoost = 0;
69107
+ for (const tok of hintTokens) {
69108
+ if (hay.includes(tok)) {
69109
+ hintBoost += 2;
69110
+ if (hintBoost >= 6) break;
69111
+ }
69112
+ }
69113
+ score += hintBoost;
69114
+ }
68288
69115
  return score;
68289
69116
  }
68290
- function rankSeeds(seeds) {
68291
- return seeds.map((symbol, i) => ({ symbol, score: scoreSeed(symbol, i), role: seedRole(symbol), i })).sort((a, b2) => b2.score === a.score ? a.i - b2.i : b2.score - a.score).map(({ symbol, score, role }) => ({ symbol, score, role }));
69117
+ function rankSeeds(seeds, hintTokens) {
69118
+ return seeds.map((symbol, i) => ({ symbol, score: scoreSeed(symbol, i, hintTokens), role: seedRole(symbol), i })).sort((a, b2) => b2.score === a.score ? a.i - b2.i : b2.score - a.score).map(({ symbol, score, role }) => ({ symbol, score, role }));
68292
69119
  }
68293
69120
 
68294
69121
  // ../../packages/engine/src/normalize.ts
@@ -68348,6 +69175,7 @@ var RUNTIME_KINDS2 = /* @__PURE__ */ new Set([
68348
69175
  ]);
68349
69176
  var MAX_RUNTIME_CONTRIBUTION = 2;
68350
69177
  var MAX_STRUCTURAL_CONTRIBUTION = 0.6;
69178
+ var MAX_AMBIENT_RUNTIME_CONTRIBUTION = 0.6;
68351
69179
  var NORMALIZATION = 6;
68352
69180
  function clamp014(n) {
68353
69181
  if (Number.isNaN(n)) return 0;
@@ -68355,13 +69183,18 @@ function clamp014(n) {
68355
69183
  if (n > 1) return 1;
68356
69184
  return n;
68357
69185
  }
68358
- function computeWeightedEvidenceConfidence(evidence2) {
69186
+ function computeWeightedEvidenceConfidence(evidence2, ambientEvidenceIds) {
68359
69187
  const runtimeBySource = /* @__PURE__ */ new Map();
69188
+ const ambientBySource = /* @__PURE__ */ new Map();
68360
69189
  const structuralBySource = /* @__PURE__ */ new Map();
68361
69190
  for (const e of evidence2) {
68362
69191
  const r = clamp014(e.relevance);
68363
69192
  if (RUNTIME_KINDS2.has(e.kind)) {
68364
- runtimeBySource.set(e.source, (runtimeBySource.get(e.source) ?? 0) + 1.5 * r);
69193
+ if (ambientEvidenceIds?.has(e.id)) {
69194
+ ambientBySource.set(e.source, (ambientBySource.get(e.source) ?? 0) + 0.5 * r);
69195
+ } else {
69196
+ runtimeBySource.set(e.source, (runtimeBySource.get(e.source) ?? 0) + 1.5 * r);
69197
+ }
68365
69198
  } else {
68366
69199
  structuralBySource.set(e.source, (structuralBySource.get(e.source) ?? 0) + 0.5 * r);
68367
69200
  }
@@ -68370,11 +69203,15 @@ function computeWeightedEvidenceConfidence(evidence2) {
68370
69203
  (acc, w) => acc + Math.min(w, MAX_RUNTIME_CONTRIBUTION),
68371
69204
  0
68372
69205
  );
69206
+ const ambientSum = [...ambientBySource.values()].reduce(
69207
+ (acc, w) => acc + Math.min(w, MAX_AMBIENT_RUNTIME_CONTRIBUTION),
69208
+ 0
69209
+ );
68373
69210
  const structuralSum = [...structuralBySource.values()].reduce(
68374
69211
  (acc, w) => acc + Math.min(w, MAX_STRUCTURAL_CONTRIBUTION),
68375
69212
  0
68376
69213
  );
68377
- return clamp014((runtimeSum + structuralSum) / NORMALIZATION);
69214
+ return clamp014((runtimeSum + ambientSum + structuralSum) / NORMALIZATION);
68378
69215
  }
68379
69216
 
68380
69217
  // ../../packages/engine/src/git-collector.ts
@@ -68390,8 +69227,9 @@ function isRefLike(s) {
68390
69227
  if (t.startsWith("-")) return false;
68391
69228
  if (t.includes("..")) return false;
68392
69229
  if (/\s|:|ago|yesterday|week|month|year|=/i.test(t)) return false;
69230
+ if (/^\d+[smhd]$/i.test(t)) return false;
68393
69231
  if (/^[0-9a-f]{7,40}$/i.test(t)) return true;
68394
- if (/^[a-zA-Z0-9_./-]{1,200}$/.test(t) && !/^\d{4}-\d{2}-\d{2}$/.test(t)) return true;
69232
+ if (/^[a-zA-Z0-9_.~^/-]{1,200}$/.test(t) && !/^\d{4}-\d{2}-\d{2}$/.test(t)) return true;
68395
69233
  return false;
68396
69234
  }
68397
69235
  function parseDiffStat(stdout) {
@@ -68610,6 +69448,33 @@ function logWindowFrom(since) {
68610
69448
  }
68611
69449
  return new Date(now - 7 * 864e5).toISOString();
68612
69450
  }
69451
+ function classifyLogRelevance(sigKey, sigServices, seedTerms, inputService) {
69452
+ if (seedTerms.length === 0) {
69453
+ return { relevanceClass: "direct", relevanceReason: "no seed context \u2014 included by default" };
69454
+ }
69455
+ const keyTokens = tokenize(sigKey);
69456
+ const serviceTokens = sigServices.flatMap((s) => tokenize(s));
69457
+ const combined = [...keyTokens, ...serviceTokens];
69458
+ const matchingTerms = seedTerms.filter(
69459
+ (t) => combined.some((c) => c.includes(t) || t.includes(c))
69460
+ );
69461
+ if (matchingTerms.length > 0) {
69462
+ return {
69463
+ relevanceClass: "direct",
69464
+ relevanceReason: `matches seed terms: ${matchingTerms.slice(0, 3).join(", ")}`
69465
+ };
69466
+ }
69467
+ if (inputService && sigServices.some((s) => s.toLowerCase().includes(inputService.toLowerCase()))) {
69468
+ return {
69469
+ relevanceClass: "direct",
69470
+ relevanceReason: `from configured service: ${inputService}`
69471
+ };
69472
+ }
69473
+ return {
69474
+ relevanceClass: "ambient",
69475
+ relevanceReason: "no structural link to seed \u2014 ambient runtime noise"
69476
+ };
69477
+ }
68613
69478
  function queueFindingConfidence(opts) {
68614
69479
  const { starvedCount, backloggedCount, failingCount } = opts;
68615
69480
  return starvedCount > 0 && backloggedCount === 0 && failingCount === 0 ? 0.65 : 0.85;
@@ -68618,7 +69483,8 @@ function looksDiffable(since) {
68618
69483
  const s = since.trim();
68619
69484
  if (s.length === 0) return false;
68620
69485
  if (s.includes("..")) return true;
68621
- return /^[A-Za-z0-9._/-]+$/.test(s);
69486
+ if (/^\d+[smhd]$/i.test(s)) return false;
69487
+ return /^[A-Za-z0-9._/~^-]+$/.test(s);
68622
69488
  }
68623
69489
  async function investigate(input, deps) {
68624
69490
  const { code, db } = deps;
@@ -68641,7 +69507,8 @@ async function investigate(input, deps) {
68641
69507
  return ev;
68642
69508
  }
68643
69509
  const rawSeeds = await code.searchSymbols(hint, 5);
68644
- const ranked = rankSeeds(rawSeeds);
69510
+ const hintTokens = [...new Set(tokenize(hint))];
69511
+ const ranked = rankSeeds(rawSeeds, hintTokens);
68645
69512
  const seeds = ranked.map((r) => r.symbol);
68646
69513
  const top = seeds[0];
68647
69514
  if (!top) {
@@ -68693,6 +69560,13 @@ async function investigate(input, deps) {
68693
69560
  changes = null;
68694
69561
  }
68695
69562
  }
69563
+ let recentChanges;
69564
+ if (deps.repoPath && input.since) {
69565
+ try {
69566
+ recentChanges = await collectGitChanges({ repoPath: deps.repoPath, since: input.since });
69567
+ } catch {
69568
+ }
69569
+ }
68696
69570
  const seedLine = top.startLine ?? 0;
68697
69571
  const seedEv = mkEv(
68698
69572
  "symbol",
@@ -68750,8 +69624,20 @@ async function investigate(input, deps) {
68750
69624
  );
68751
69625
  changeEvId = ev.id;
68752
69626
  }
69627
+ if (changes === null && recentChanges !== void 0 && recentChanges.commits.length > 0) {
69628
+ const { commits, changedFiles, totalInsertions, totalDeletions } = recentChanges;
69629
+ const ev = mkEv(
69630
+ "commit",
69631
+ `Git history ${input.since}..HEAD: ${commits.length} commit(s), ${changedFiles.length} file(s) changed (+${totalInsertions} -${totalDeletions})`,
69632
+ { commits: commits.slice(0, 10), changedFiles: changedFiles.slice(0, 30), totalInsertions, totalDeletions, source: "git" },
69633
+ {}
69634
+ );
69635
+ changeEvId = ev.id;
69636
+ }
68753
69637
  let analysis = null;
68754
69638
  const logEvIds = [];
69639
+ const directLogEvIds = [];
69640
+ const ambientLogEvIds = [];
68755
69641
  let logsCollected = false;
68756
69642
  let logsCompatibilityError;
68757
69643
  if (deps.logs) {
@@ -68768,8 +69654,19 @@ async function investigate(input, deps) {
68768
69654
  const from = logWindowFrom(input.since);
68769
69655
  analysis = await deps.logs.analyzeErrors({ service: input.service, from });
68770
69656
  logsCollected = true;
69657
+ const seedBase = top.filePath.split("/").pop() ?? "";
69658
+ const logSeedTerms = [
69659
+ .../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(seedBase)])
69660
+ ];
68771
69661
  for (const s of analysis.signatures.slice(0, 15)) {
69662
+ const { relevanceClass, relevanceReason } = classifyLogRelevance(
69663
+ s.key,
69664
+ s.services,
69665
+ logSeedTerms,
69666
+ input.service
69667
+ );
68772
69668
  const tags = [];
69669
+ if (relevanceClass === "ambient") tags.push("ambient");
68773
69670
  if (s.isNew) tags.push("NEW");
68774
69671
  else if (s.ratio !== void 0 && Number.isFinite(s.ratio) && s.ratio >= 1.5) {
68775
69672
  tags.push(`spike x${s.ratio.toFixed(1)}`);
@@ -68790,15 +69687,21 @@ async function investigate(input, deps) {
68790
69687
  services: s.services,
68791
69688
  isNew: s.isNew ?? false,
68792
69689
  ratio: s.ratio ?? null,
68793
- sampleMessage: s.sampleMessage ?? null
69690
+ sampleMessage: s.sampleMessage ?? null,
69691
+ relevanceClass,
69692
+ relevanceReason
68794
69693
  },
68795
69694
  {},
68796
69695
  s.lastSeen || void 0,
68797
- s.isNew ? 0.95 : s.ratio !== void 0 && s.ratio >= 1.5 ? 0.9 : 0.8
69696
+ // Direct evidence: full recurrence weight. Ambient: demoted baseline.
69697
+ // This prevents unrelated high-volume errors from inflating confidence.
69698
+ relevanceClass === "direct" ? s.isNew ? 0.95 : s.ratio !== void 0 && s.ratio >= 1.5 ? 0.9 : 0.85 : s.isNew ? 0.7 : s.ratio !== void 0 && s.ratio >= 1.5 ? 0.55 : 0.35
68798
69699
  );
68799
69700
  if (s.isNew) ev.isNew = s.isNew;
68800
69701
  if (typeof s.ratio === "number" && Number.isFinite(s.ratio)) ev.ratio = s.ratio;
68801
69702
  logEvIds.push(ev.id);
69703
+ if (relevanceClass === "direct") directLogEvIds.push(ev.id);
69704
+ else ambientLogEvIds.push(ev.id);
68802
69705
  }
68803
69706
  }
68804
69707
  } catch {
@@ -68811,11 +69714,8 @@ async function investigate(input, deps) {
68811
69714
  if (deps.mongo) {
68812
69715
  try {
68813
69716
  stateAnalysis = await deps.mongo.analyzeState();
68814
- const seedBase = top.filePath.split("/").pop() ?? "";
68815
- const terms = [
68816
- .../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(seedBase)])
68817
- ];
68818
- for (const s of selectStateSignals(stateAnalysis, terms)) {
69717
+ const stateTerms = [...new Set(tokenize(hint))];
69718
+ for (const s of selectStateSignals(stateAnalysis, stateTerms)) {
68819
69719
  const ev = mkEv("state", s.title, s.payload, {}, s.timestamp, s.relevance);
68820
69720
  stateEvIds.push(ev.id);
68821
69721
  stateCollections.add(s.collection);
@@ -68853,12 +69753,13 @@ async function investigate(input, deps) {
68853
69753
  queueRuntimeState = null;
68854
69754
  }
68855
69755
  }
68856
- const METRICS_TIMEOUT_MS = 1e4;
69756
+ const METRICS_TIMEOUT_MS = 3e4;
68857
69757
  const metricEvIds = [];
68858
69758
  const latencyMetricEvIds = [];
68859
69759
  const queueMetricEvIds = [];
68860
69760
  const queueMetricEvIdsByQueue = /* @__PURE__ */ new Map();
68861
69761
  let metricsCollected = false;
69762
+ let metricsFailureReason;
68862
69763
  if (deps.metrics) {
68863
69764
  const ac = new AbortController();
68864
69765
  let metricsTimerId;
@@ -68919,7 +69820,8 @@ async function investigate(input, deps) {
68919
69820
  }
68920
69821
  }
68921
69822
  metricsCollected = true;
68922
- } catch {
69823
+ } catch (metricsErr) {
69824
+ metricsFailureReason = metricsErr?.message?.slice(0, 120) ?? "unknown error";
68923
69825
  } finally {
68924
69826
  if (metricsTimerId !== void 0) clearTimeout(metricsTimerId);
68925
69827
  }
@@ -68947,9 +69849,12 @@ async function investigate(input, deps) {
68947
69849
  latencyMetricEvIds,
68948
69850
  queueBacklogEvIdsByQueue,
68949
69851
  queueStarvationEvIdsByQueue,
68950
- queueMetricEvIdsByQueue
69852
+ queueMetricEvIdsByQueue,
69853
+ sinceProvided: input.since !== void 0,
69854
+ graph
68951
69855
  });
68952
69856
  const validated = validateHypotheses(hyps, evidence2);
69857
+ const causeChains = buildCauseChains(validated, evidence2, graph, label);
68953
69858
  const findings2 = [];
68954
69859
  findings2.push({
68955
69860
  kind: "observation",
@@ -69004,24 +69909,43 @@ async function investigate(input, deps) {
69004
69909
  confidence: clamp015(0.4 + Math.min(m, 20) / 40),
69005
69910
  evidenceIds: [changeEvId]
69006
69911
  });
69912
+ } else if (!changes && changeEvId && recentChanges) {
69913
+ findings2.push({
69914
+ kind: "observation",
69915
+ title: `${recentChanges.commits.length} commit(s) in ${input.since}..HEAD touching ${recentChanges.changedFiles.length} file(s)`,
69916
+ confidence: clamp015(0.3 + Math.min(recentChanges.commits.length, 10) / 20),
69917
+ evidenceIds: [changeEvId]
69918
+ });
69007
69919
  }
69008
69920
  if (analysis !== null && analysis.signatures.length > 0) {
69009
69921
  const newN = analysis.newSignatures.length;
69010
69922
  const affected = analysis.affectedServices.length > 0 ? analysis.affectedServices.join(", ") : input.service ?? "the service";
69923
+ const hasDirectEvidence = directLogEvIds.length > 0;
69924
+ const ambientCount = ambientLogEvIds.length;
69925
+ const ambientSuffix = !hasDirectEvidence && ambientCount > 0 ? ` (${ambientCount} ambient \u2014 no direct link to seed established)` : ambientCount > 0 ? ` (${directLogEvIds.length} direct, ${ambientCount} ambient)` : "";
69011
69926
  findings2.push({
69012
69927
  kind: "observation",
69013
- title: `${analysis.signatures.length} error signature(s) (${newN} new, ${analysis.totalErrors} error(s)) \u2014 affected: ${affected}`,
69014
- confidence: 0.65,
69928
+ title: `${analysis.signatures.length} error signature(s) (${newN} new, ${analysis.totalErrors} error(s)) \u2014 affected: ${affected}${ambientSuffix}`,
69929
+ confidence: hasDirectEvidence ? 0.65 : 0.4,
69015
69930
  evidenceIds: logEvIds
69016
69931
  });
69017
- const top2 = analysis.signatures[0];
69018
- if (top2 !== void 0) {
69019
- const flag = top2.isNew ? " (NEW)" : top2.ratio !== void 0 && Number.isFinite(top2.ratio) && top2.ratio >= 1.5 ? ` (spike x${top2.ratio.toFixed(1)})` : "";
69932
+ const topDirectId = directLogEvIds[0];
69933
+ const topSig = analysis.signatures[0];
69934
+ if (topDirectId !== void 0 && topSig !== void 0) {
69935
+ const flag = topSig.isNew ? " (NEW)" : topSig.ratio !== void 0 && Number.isFinite(topSig.ratio) && topSig.ratio >= 1.5 ? ` (spike x${topSig.ratio.toFixed(1)})` : "";
69020
69936
  findings2.push({
69021
69937
  kind: "anomaly",
69022
- title: `Top error signature: ${top2.key} \u2014 ${top2.count}x${flag}, last ${shortTs(top2.lastSeen)}`,
69938
+ title: `Top error signature: ${topSig.key} \u2014 ${topSig.count}x${flag}, last ${shortTs(topSig.lastSeen)}`,
69023
69939
  confidence: 0.7,
69024
- evidenceIds: logEvIds.slice(0, 1)
69940
+ evidenceIds: [topDirectId]
69941
+ });
69942
+ } else if (topSig !== void 0 && ambientCount > 0) {
69943
+ const flag = topSig.isNew ? " (NEW)" : topSig.ratio !== void 0 && Number.isFinite(topSig.ratio) && topSig.ratio >= 1.5 ? ` (spike x${topSig.ratio.toFixed(1)})` : "";
69944
+ findings2.push({
69945
+ kind: "observation",
69946
+ title: `Top error signature (ambient): ${topSig.key} \u2014 ${topSig.count}x${flag} \u2014 no structural link to seed`,
69947
+ confidence: 0.35,
69948
+ evidenceIds: ambientLogEvIds.slice(0, 1)
69025
69949
  });
69026
69950
  }
69027
69951
  }
@@ -69119,13 +70043,17 @@ async function investigate(input, deps) {
69119
70043
  metadata: { blastRadius }
69120
70044
  });
69121
70045
  }
69122
- if (changes && changeEvId) {
70046
+ if (changeEvId) {
70047
+ const gitChangedFiles = recentChanges?.changedFiles ?? [];
70048
+ const seedInChanges = gitChangedFiles.length > 0 && gitChangedFiles.some(
70049
+ (f) => f === top.filePath || top.filePath.endsWith(f) || f.endsWith(top.filePath)
70050
+ );
69123
70051
  causeInputs.push({
69124
70052
  id: "cause:deployment-regression",
69125
70053
  title: `Recent change to ${top.name} in ${input.since}..HEAD may have introduced the regression`,
69126
70054
  category: "deployment-regression",
69127
70055
  sourceEvidenceIds: [changeEvId, seedEv.id],
69128
- baseScore: clamp015(0.25 + (queueHits.length > 0 ? 0.05 : 0)),
70056
+ baseScore: clamp015((seedInChanges ? 0.45 : 0.25) + (queueHits.length > 0 ? 0.05 : 0)),
69129
70057
  metadata: { blastRadius }
69130
70058
  });
69131
70059
  }
@@ -69139,7 +70067,7 @@ async function investigate(input, deps) {
69139
70067
  metadata: { blastRadius }
69140
70068
  });
69141
70069
  }
69142
- if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0) {
70070
+ if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0 && directLogEvIds.length > 0) {
69143
70071
  const firstQueue = queueHits[0];
69144
70072
  const queueLabel = firstQueue !== void 0 ? `"${firstQueue.queueName}" (${firstQueue.producerSymbol ?? "unknown"} -> ${firstQueue.workerSymbol ?? "unknown"})` : "the queue path";
69145
70073
  const topSig = analysis.signatures[0];
@@ -69147,7 +70075,7 @@ async function investigate(input, deps) {
69147
70075
  id: "cause:error-correlation",
69148
70076
  title: `Runtime errors (${analysis.totalErrors}${topSig ? `, top ${topSig.key}` : ""}) correlate with the implicated queue path ${queueLabel}`,
69149
70077
  category: "error-correlation",
69150
- sourceEvidenceIds: logEvIds.slice(0, 3),
70078
+ sourceEvidenceIds: directLogEvIds.slice(0, 3),
69151
70079
  baseScore: 0.3,
69152
70080
  metadata: { blastRadius }
69153
70081
  });
@@ -69186,19 +70114,15 @@ async function investigate(input, deps) {
69186
70114
  providerReliability,
69187
70115
  request: { hint: input.hint, service: input.service }
69188
70116
  });
69189
- const evidenceConfidence = computeWeightedEvidenceConfidence(evidence2);
70117
+ const evidenceConfidence = computeWeightedEvidenceConfidence(
70118
+ evidence2,
70119
+ ambientLogEvIds.length > 0 ? new Set(ambientLogEvIds) : void 0
70120
+ );
69190
70121
  const seedResolved = seeds.length > 0 ? 1 : 0;
69191
70122
  const confidence = clamp015(0.5 * evidenceConfidence + 0.5 * seedResolved);
69192
70123
  const area = ctx.community?.name ?? top.filePath;
69193
70124
  const topCause = rankedCauses[0];
69194
70125
  const summary = topCause ? `Investigation of "${hint}" resolved to ${label} (${area}). Top suspected cause: ${topCause.title}.` : `Investigation of "${hint}" resolved to ${label} (${area}). No dominant suspected cause emerged from the available structural evidence.`;
69195
- let recentChanges;
69196
- if (deps.repoPath && input.since) {
69197
- try {
69198
- recentChanges = await collectGitChanges({ repoPath: deps.repoPath, since: input.since });
69199
- } catch {
69200
- }
69201
- }
69202
70126
  const nextActions = buildNextActions(top, ctx, impact, queueHits, changes, input);
69203
70127
  if (ownershipEstimate?.likelyMaintainer) {
69204
70128
  nextActions.unshift(
@@ -69222,15 +70146,18 @@ async function investigate(input, deps) {
69222
70146
  confidence,
69223
70147
  nextActions,
69224
70148
  ownership: ownershipEstimate,
70149
+ causeChains: causeChains.length > 0 ? causeChains : void 0,
69225
70150
  ...recentChanges !== void 0 ? { recentChanges } : {}
69226
70151
  };
69227
- const connectorFlags = deps.connectors ? { ...deps.connectors, metricsCollected, logsCollected, logsCompatibilityError } : {
70152
+ const connectorFlags = deps.connectors ? { ...deps.connectors, metricsCollected, metricsFailureReason, logsCollected, logsCompatibilityError, sinceProvided: input.since !== void 0 } : {
69228
70153
  elasticsearch: deps.logs != null,
69229
70154
  mongodb: deps.mongo != null,
69230
70155
  grafana: deps.metrics != null,
69231
70156
  metricsCollected,
70157
+ metricsFailureReason,
69232
70158
  logsCollected,
69233
- logsCompatibilityError
70159
+ logsCompatibilityError,
70160
+ sinceProvided: input.since !== void 0
69234
70161
  };
69235
70162
  const gapAnalysis = detectMissingEvidence(report, connectorFlags);
69236
70163
  report.gapAnalysis = gapAnalysis;
@@ -69374,11 +70301,11 @@ function groupQueueEvidence(evidence2) {
69374
70301
  return map2;
69375
70302
  }
69376
70303
  function queueEvidenceIds(evidence2) {
69377
- const ids = /* @__PURE__ */ new Set();
70304
+ const ids2 = /* @__PURE__ */ new Set();
69378
70305
  for (const e of evidence2) {
69379
- if (e.source === "queue" && e.kind === "queue-state") ids.add(e.id);
70306
+ if (e.source === "queue" && e.kind === "queue-state") ids2.add(e.id);
69380
70307
  }
69381
- return ids;
70308
+ return ids2;
69382
70309
  }
69383
70310
  var CONFIDENCE_EXPLAIN_THRESHOLD = 0.8;
69384
70311
  function explainLowConfidence(r) {
@@ -69541,6 +70468,12 @@ function renderReport2(r) {
69541
70468
  }
69542
70469
  lines.push("");
69543
70470
  lines.push("## Hypotheses");
70471
+ const allUnconfirmed = r.hypotheses.length > 0 && r.hypotheses.every((h) => h.verdict === "unconfirmed");
70472
+ if (allUnconfirmed) {
70473
+ lines.push(
70474
+ "_All hypotheses below are unconfirmed placeholders \u2014 runtime evidence is required to validate them._"
70475
+ );
70476
+ }
69544
70477
  if (r.hypotheses.length === 0) {
69545
70478
  lines.push("(none)");
69546
70479
  } else {
@@ -69555,6 +70488,19 @@ function renderReport2(r) {
69555
70488
  }
69556
70489
  }
69557
70490
  lines.push("");
70491
+ if (r.causeChains !== void 0 && r.causeChains.length > 0) {
70492
+ lines.push("## Cause chains");
70493
+ for (const chain of r.causeChains) {
70494
+ lines.push(` [${chain.confidence.toFixed(2)}] ${chain.category}: ${chain.summary}`);
70495
+ for (let i = 0; i < chain.steps.length; i++) {
70496
+ const step = chain.steps[i];
70497
+ const prefix = i === chain.steps.length - 1 ? " \u2514\u2500" : " \u251C\u2500";
70498
+ const evCite = step.evidenceIds.length > 0 ? ` (evidence: ${step.evidenceIds.map(shortId).join(", ")})` : "";
70499
+ lines.push(`${prefix} [${step.role}] ${step.label}${evCite}`);
70500
+ }
70501
+ }
70502
+ lines.push("");
70503
+ }
69558
70504
  lines.push("## Evidence gaps (what we don't know)");
69559
70505
  if (r.gapAnalysis.gaps.length === 0) {
69560
70506
  lines.push("(no major evidence gaps)");
@@ -69685,6 +70631,12 @@ function reportToMarkdown(r) {
69685
70631
  }
69686
70632
  out.push("");
69687
70633
  out.push("## Hypotheses");
70634
+ const mdAllUnconfirmed = r.hypotheses.length > 0 && r.hypotheses.every((h) => h.verdict === "unconfirmed");
70635
+ if (mdAllUnconfirmed) {
70636
+ out.push(
70637
+ "_All hypotheses below are unconfirmed placeholders \u2014 runtime evidence is required to validate them._"
70638
+ );
70639
+ }
69688
70640
  if (r.hypotheses.length === 0) {
69689
70641
  out.push("_none_");
69690
70642
  } else {
@@ -69900,7 +70852,8 @@ async function reconstructChangeTimeline(input, deps) {
69900
70852
  }
69901
70853
  }
69902
70854
  }
69903
- const summary = commits.length + " commit(s)" + (service !== void 0 ? " touching " + service : "") + " in window" + (input.since !== void 0 ? " since " + input.since : "") + (impact !== null ? "; " + impact.summary : "") + ".";
70855
+ const impactPart = impact !== null ? "; " + (impact.summary.endsWith(".") ? impact.summary.slice(0, -1) : impact.summary) : "";
70856
+ const summary = commits.length + " commit(s)" + (service !== void 0 ? " touching " + service : "") + " in window" + (input.since !== void 0 ? " since " + input.since : "") + impactPart + ".";
69904
70857
  const note = "Changes are evidence, not conclusions \u2014 a change in this window is not automatically the cause.";
69905
70858
  return {
69906
70859
  window: {
@@ -69923,6 +70876,9 @@ function renderChangeTimeline(t) {
69923
70876
  lines.push("# Change Timeline");
69924
70877
  lines.push("");
69925
70878
  lines.push("## Summary");
70879
+ const sinceLabel = t.window.since ?? "(all history)";
70880
+ const untilLabel = t.window.until ?? "HEAD";
70881
+ lines.push("Range: " + sinceLabel + " \u2192 " + untilLabel);
69926
70882
  lines.push(t.summary);
69927
70883
  lines.push("");
69928
70884
  lines.push("> " + t.note);
@@ -69945,9 +70901,13 @@ function renderChangeTimeline(t) {
69945
70901
  if (t.changeImpact !== null) {
69946
70902
  lines.push("");
69947
70903
  lines.push("## Change impact");
69948
- lines.push(
69949
- t.changeImpact.summary + " Affected flows: " + t.changeImpact.affectedFlows.length + "."
69950
- );
70904
+ lines.push("Git range: " + t.changeImpact.base + ".." + t.changeImpact.compare);
70905
+ lines.push(t.changeImpact.summary);
70906
+ if (t.changeImpact.affectedFlows.length > 0) {
70907
+ for (const f of t.changeImpact.affectedFlows) {
70908
+ lines.push(" - " + f.flowName);
70909
+ }
70910
+ }
69951
70911
  }
69952
70912
  return lines.join("\n");
69953
70913
  }
@@ -70007,6 +70967,9 @@ function renderWhatChanged(r) {
70007
70967
  const lines = [];
70008
70968
  lines.push("# What changed");
70009
70969
  lines.push("");
70970
+ const sinceLabel = r.window.since ?? "(all history)";
70971
+ const untilLabel = r.window.until ?? "HEAD";
70972
+ lines.push("Range: " + sinceLabel + " \u2192 " + untilLabel);
70010
70973
  lines.push(r.summary);
70011
70974
  lines.push("");
70012
70975
  lines.push("> " + r.note);
@@ -70036,6 +70999,11 @@ function renderWhatChanged(r) {
70036
70999
  lines.push(
70037
71000
  "## Affected flows: " + r.changeImpact.affectedFlows.length + " execution flow(s) affected"
70038
71001
  );
71002
+ if (r.changeImpact.affectedFlows.length > 0) {
71003
+ for (const f of r.changeImpact.affectedFlows) {
71004
+ lines.push(" - " + f.flowName);
71005
+ }
71006
+ }
70039
71007
  }
70040
71008
  return lines.join("\n");
70041
71009
  }
@@ -70166,7 +71134,7 @@ async function discoverArchitecture(deps) {
70166
71134
  })();
70167
71135
  const largestSubsystem = subsystems[0];
70168
71136
  const largestDesc = largestSubsystem != null ? `${largestSubsystem.name} with ${largestSubsystem.members} symbols` : "none";
70169
- const summary = `${subsystems.length} subsystems (largest: ${largestDesc}), ${asyncBoundaries.length} async queue boundaries, ${externalSystems.length} external systems, ${deadCode} dead-code symbols.`;
71137
+ const summary = `${subsystems.length} subsystems (largest: ${largestDesc}), ${asyncBoundaries.length} async queue boundaries, ${externalSystems.length} external systems, ${deadCode} unreferenced symbols.`;
70170
71138
  return {
70171
71139
  nodeStats,
70172
71140
  subsystems,
@@ -70234,7 +71202,7 @@ function renderArchitecture(m) {
70234
71202
  }
70235
71203
  lines.push("");
70236
71204
  lines.push("## Fragility");
70237
- lines.push(`- Dead-code symbols: ${m.fragile.deadCode}`);
71205
+ lines.push(`- Unreferenced symbols: ${m.fragile.deadCode}`);
70238
71206
  lines.push(`- High-coupling pairs (co-changes \u2265 3): ${m.fragile.highCouplingPairs}`);
70239
71207
  return lines.join("\n");
70240
71208
  }
@@ -70425,6 +71393,13 @@ function renderOwnership(o) {
70425
71393
  lines.push("");
70426
71394
  if (o.file === null) {
70427
71395
  lines.push(o.note);
71396
+ if (o.candidates !== void 0 && o.candidates.length > 0) {
71397
+ lines.push("");
71398
+ lines.push("Candidates:");
71399
+ for (const c of o.candidates) {
71400
+ lines.push(" " + c);
71401
+ }
71402
+ }
70428
71403
  return lines.join("\n");
70429
71404
  }
70430
71405
  if (o.symbol !== null) {
@@ -70550,17 +71525,35 @@ function generatePostmortem(r) {
70550
71525
  );
70551
71526
  }
70552
71527
  } else {
70553
- if (supportedHypotheses.length > 0) {
70554
- lines.push("The following factors are supported by the collected evidence:");
71528
+ if (r.causeChains !== void 0 && r.causeChains.length > 0) {
71529
+ lines.push("The following causal sequences are supported by the collected evidence:");
70555
71530
  lines.push("");
70556
- for (const h of supportedHypotheses) {
70557
- lines.push(
70558
- `- **${h.category}:** ${h.statement} _(confidence ${h.confidence.toFixed(2)})_`
70559
- );
71531
+ for (const chain of r.causeChains) {
71532
+ lines.push(`### ${chain.category} _(confidence ${chain.confidence.toFixed(2)})_`);
71533
+ lines.push("");
71534
+ lines.push(chain.summary);
71535
+ lines.push("");
71536
+ for (let i = 0; i < chain.steps.length; i++) {
71537
+ const step = chain.steps[i];
71538
+ const num = i + 1;
71539
+ const evCite = step.evidenceIds.length > 0 ? ` \u2014 evidence: ${step.evidenceIds.map((id) => `\`${shortId2(id)}\``).join(", ")}` : "";
71540
+ lines.push(`${num}. **[${step.role}]** ${step.label}${evCite}`);
71541
+ }
71542
+ lines.push("");
71543
+ }
71544
+ } else {
71545
+ if (supportedHypotheses.length > 0) {
71546
+ lines.push("The following factors are supported by the collected evidence:");
71547
+ lines.push("");
71548
+ for (const h of supportedHypotheses) {
71549
+ lines.push(
71550
+ `- **${h.category}:** ${h.statement} _(confidence ${h.confidence.toFixed(2)})_`
71551
+ );
71552
+ }
70560
71553
  }
70561
71554
  }
70562
71555
  if (commitEvidence.length > 0) {
70563
- if (supportedHypotheses.length > 0) lines.push("");
71556
+ if (supportedHypotheses.length > 0 || (r.causeChains?.length ?? 0) > 0) lines.push("");
70564
71557
  lines.push("Recent changes (commit evidence) present during this incident:");
70565
71558
  lines.push("");
70566
71559
  for (const e of commitEvidence) {
@@ -70621,19 +71614,29 @@ function generatePostmortem(r) {
70621
71614
  lines.push("");
70622
71615
  lines.push("## Follow-up actions");
70623
71616
  lines.push("");
70624
- const seen = /* @__PURE__ */ new Set();
70625
71617
  const checkboxItems = [];
71618
+ const seen = /* @__PURE__ */ new Set();
70626
71619
  for (const action of r.nextActions) {
70627
71620
  if (!seen.has(action)) {
70628
71621
  seen.add(action);
70629
71622
  checkboxItems.push(action);
70630
71623
  }
70631
71624
  }
71625
+ const seenGapSources = /* @__PURE__ */ new Set();
70632
71626
  for (const gap of r.gapAnalysis.gaps) {
70633
- const wireAction = `Wire ${gap.nextSource} to close the '${gap.dimension}' evidence gap`;
70634
- if (!seen.has(wireAction)) {
70635
- seen.add(wireAction);
70636
- checkboxItems.push(wireAction);
71627
+ if (seenGapSources.has(gap.nextSource)) continue;
71628
+ seenGapSources.add(gap.nextSource);
71629
+ const cleanGapAction = `${gap.nextSource} to close the '${gap.dimension}' evidence gap`;
71630
+ if (seen.has(gap.nextSource)) {
71631
+ const idx = checkboxItems.indexOf(gap.nextSource);
71632
+ if (idx !== -1 && !seen.has(cleanGapAction)) {
71633
+ checkboxItems[idx] = cleanGapAction;
71634
+ seen.delete(gap.nextSource);
71635
+ seen.add(cleanGapAction);
71636
+ }
71637
+ } else if (!seen.has(cleanGapAction)) {
71638
+ seen.add(cleanGapAction);
71639
+ checkboxItems.push(cleanGapAction);
70637
71640
  }
70638
71641
  }
70639
71642
  if (checkboxItems.length === 0) {
@@ -70765,11 +71768,113 @@ var TOPIC_MAP = {
70765
71768
  }
70766
71769
  };
70767
71770
  var ALL_TOPICS = Object.keys(TOPIC_MAP);
71771
+ var STOP_WORDS = /* @__PURE__ */ new Set([
71772
+ "on",
71773
+ "the",
71774
+ "a",
71775
+ "an",
71776
+ "at",
71777
+ "in",
71778
+ "of",
71779
+ "and",
71780
+ "or",
71781
+ "for",
71782
+ "with",
71783
+ "by",
71784
+ "to",
71785
+ "from",
71786
+ "that",
71787
+ "this",
71788
+ "is",
71789
+ "are",
71790
+ "was",
71791
+ "were",
71792
+ "it",
71793
+ "be",
71794
+ "about",
71795
+ "focus",
71796
+ "ignore",
71797
+ "exclude",
71798
+ "only",
71799
+ "just",
71800
+ "concentrate",
71801
+ "look",
71802
+ "without",
71803
+ "skip",
71804
+ "drop"
71805
+ ]);
71806
+ function hasFocusVerb(d) {
71807
+ return /\b(focus|only|just|concentrate|look at)\b/.test(d);
71808
+ }
71809
+ function hasIgnoreVerb(d) {
71810
+ return /\b(ignore|exclude|without|skip|drop)\b/.test(d);
71811
+ }
70768
71812
  function detectMode(d) {
70769
71813
  if (/\b(ignore|exclude|without|skip|drop)\b/.test(d)) return "ignore";
70770
71814
  if (/\b(focus|only|just|concentrate|look at)\b/.test(d)) return "focus";
70771
71815
  return "none";
70772
71816
  }
71817
+ function splitIntoClauses(d) {
71818
+ const parts = d.split(/\s+and\s+|\s*,\s*/);
71819
+ const clauses = [];
71820
+ for (const part of parts) {
71821
+ const trimmed = part.trim();
71822
+ if (hasFocusVerb(trimmed)) clauses.push({ clause: trimmed, mode: "focus" });
71823
+ else if (hasIgnoreVerb(trimmed)) clauses.push({ clause: trimmed, mode: "ignore" });
71824
+ }
71825
+ return clauses;
71826
+ }
71827
+ function applyMixedDirective(r, directive, d) {
71828
+ const clauses = splitIntoClauses(d);
71829
+ const focusClauses = clauses.filter((c) => c.mode === "focus");
71830
+ const ignoreClauses = clauses.filter((c) => c.mode === "ignore");
71831
+ const focusTopics = [...new Set(focusClauses.flatMap((c) => matchedTopics(c.clause)))];
71832
+ const ignoreTopics = [...new Set(ignoreClauses.flatMap((c) => matchedTopics(c.clause)))];
71833
+ const focusTerms = focusClauses.filter((c) => matchedTopics(c.clause).length === 0).flatMap((c) => extractSignificantTerms(c.clause));
71834
+ const ignoreTerms = ignoreClauses.filter((c) => matchedTopics(c.clause).length === 0).flatMap((c) => extractSignificantTerms(c.clause));
71835
+ let hypotheses2 = r.hypotheses;
71836
+ let evidence2 = r.evidence;
71837
+ let suspectedCauses = r.suspectedCauses;
71838
+ if (focusTopics.length > 0) {
71839
+ const focusCategories = unionCategories(focusTopics);
71840
+ const focusKinds = unionKinds(focusTopics);
71841
+ const focusKeywords = topicKeywords(focusTopics);
71842
+ hypotheses2 = hypotheses2.filter((h) => focusCategories.includes(h.category));
71843
+ evidence2 = evidence2.filter((e) => focusKinds.includes(e.kind) || e.kind === "symbol");
71844
+ const filtered = suspectedCauses.filter(
71845
+ (c) => focusKeywords.some((k) => c.title.toLowerCase().includes(k))
71846
+ );
71847
+ suspectedCauses = filtered.length > 0 ? filtered : suspectedCauses;
71848
+ } else if (focusTerms.length > 0) {
71849
+ const matchesFocus = (text2) => focusTerms.some((t) => text2.toLowerCase().includes(t));
71850
+ hypotheses2 = hypotheses2.filter((h) => matchesFocus(h.statement) || matchesFocus(h.category));
71851
+ evidence2 = evidence2.filter((e) => matchesFocus(e.title) || matchesFocus(e.kind));
71852
+ const filtered = suspectedCauses.filter((c) => matchesFocus(c.title));
71853
+ suspectedCauses = filtered.length > 0 ? filtered : suspectedCauses;
71854
+ }
71855
+ if (ignoreTopics.length > 0) {
71856
+ const ignoreCategories = unionCategories(ignoreTopics);
71857
+ const ignoreKinds = unionKinds(ignoreTopics);
71858
+ const ignoreKeywords = topicKeywords(ignoreTopics);
71859
+ hypotheses2 = hypotheses2.filter((h) => !ignoreCategories.includes(h.category));
71860
+ evidence2 = evidence2.filter((e) => !ignoreKinds.includes(e.kind));
71861
+ suspectedCauses = suspectedCauses.filter(
71862
+ (c) => !ignoreKeywords.some((k) => c.title.toLowerCase().includes(k))
71863
+ );
71864
+ }
71865
+ if (ignoreTerms.length > 0) {
71866
+ const matchesIgnore = (text2) => ignoreTerms.some((t) => text2.toLowerCase().includes(t));
71867
+ hypotheses2 = hypotheses2.filter((h) => !matchesIgnore(h.statement) && !matchesIgnore(h.category));
71868
+ evidence2 = evidence2.filter((e) => !matchesIgnore(e.title) && !matchesIgnore(e.kind));
71869
+ suspectedCauses = suspectedCauses.filter((c) => !matchesIgnore(c.title));
71870
+ }
71871
+ const allTopics = [.../* @__PURE__ */ new Set([...focusTopics, ...ignoreTopics])];
71872
+ const focusDesc = focusTopics.length > 0 ? `focus: ${focusTopics.join(", ")}` : focusTerms.length > 0 ? `focus on: ${focusTerms.join(", ")}` : null;
71873
+ const ignoreDesc = ignoreTopics.length > 0 ? `ignore: ${ignoreTopics.join(", ")}` : ignoreTerms.length > 0 ? `ignore: ${ignoreTerms.join(", ")}` : null;
71874
+ const modeDesc = [focusDesc, ignoreDesc].filter(Boolean).join("; ");
71875
+ const note = modeDesc + ". Reused the saved investigation's evidence \u2014 no re-query of production.";
71876
+ return { directive, mode: "mixed", topics: allTopics, hypotheses: hypotheses2, suspectedCauses, evidence: evidence2, note };
71877
+ }
70773
71878
  function matchedTopics(d) {
70774
71879
  return ALL_TOPICS.filter((t) => {
70775
71880
  const entry2 = TOPIC_MAP[t];
@@ -70810,11 +71915,54 @@ function topicKeywords(topics) {
70810
71915
  }
70811
71916
  return kws;
70812
71917
  }
71918
+ function extractSignificantTerms(directive) {
71919
+ return directive.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
71920
+ }
71921
+ function applyTextFilter(r, directive, mode, terms) {
71922
+ const matches = (text2) => terms.some((t) => text2.toLowerCase().includes(t));
71923
+ const note = (mode === "focus" ? "Focused on" : "Excluding") + " terms: " + terms.join(", ") + ". No predefined topic matched; filtered by text search. Reused the saved investigation's evidence \u2014 no re-query of production.";
71924
+ if (mode === "focus") {
71925
+ const hypotheses2 = r.hypotheses.filter(
71926
+ (h) => matches(h.statement) || matches(h.category)
71927
+ );
71928
+ const evidence2 = r.evidence.filter((e) => matches(e.title) || matches(e.kind));
71929
+ const suspectedCauses = r.suspectedCauses.filter((c) => matches(c.title));
71930
+ return {
71931
+ directive,
71932
+ mode: "focus",
71933
+ topics: [],
71934
+ hypotheses: hypotheses2,
71935
+ evidence: evidence2,
71936
+ suspectedCauses: suspectedCauses.length > 0 ? suspectedCauses : r.suspectedCauses,
71937
+ note
71938
+ };
71939
+ }
71940
+ return {
71941
+ directive,
71942
+ mode: "ignore",
71943
+ topics: [],
71944
+ hypotheses: r.hypotheses.filter(
71945
+ (h) => !matches(h.statement) && !matches(h.category)
71946
+ ),
71947
+ evidence: r.evidence.filter((e) => !matches(e.title) && !matches(e.kind)),
71948
+ suspectedCauses: r.suspectedCauses.filter((c) => !matches(c.title)),
71949
+ note
71950
+ };
71951
+ }
70813
71952
  function refineInvestigation(r, directive) {
70814
71953
  const d = directive.toLowerCase();
71954
+ if (hasFocusVerb(d) && hasIgnoreVerb(d)) {
71955
+ return applyMixedDirective(r, directive, d);
71956
+ }
70815
71957
  const mode = detectMode(d);
70816
71958
  const topics = matchedTopics(d);
70817
71959
  if (mode === "none" || topics.length === 0) {
71960
+ if (mode !== "none" && topics.length === 0) {
71961
+ const terms = extractSignificantTerms(d);
71962
+ if (terms.length > 0) {
71963
+ return applyTextFilter(r, directive, mode, terms);
71964
+ }
71965
+ }
70818
71966
  const recognizedList = ALL_TOPICS.join(", ");
70819
71967
  const note2 = "No specific topic directive recognized. Recognized topics: " + recognizedList + '. Example usage: "focus on queue behavior", "ignore deployment changes".';
70820
71968
  return {
@@ -70862,7 +72010,7 @@ function renderRefined(r, v) {
70862
72010
  const lines = [];
70863
72011
  lines.push("# Refined investigation \u2014 " + r.input.hint);
70864
72012
  lines.push("");
70865
- const modeLabel = v.mode === "focus" ? "focus: " + v.topics.join(", ") : v.mode === "ignore" ? "ignore: " + v.topics.join(", ") : "none (all evidence returned)";
72013
+ const modeLabel = v.mode === "focus" ? "focus: " + v.topics.join(", ") : v.mode === "ignore" ? "ignore: " + v.topics.join(", ") : v.mode === "mixed" ? "focus+ignore" + (v.topics.length > 0 ? ": " + v.topics.join(", ") : "") : "none (all evidence returned)";
70866
72014
  lines.push(" [mode: " + modeLabel + "] reusing saved evidence, no re-query");
70867
72015
  lines.push("");
70868
72016
  lines.push("## Hypotheses");
@@ -70917,20 +72065,175 @@ function refinedToJSON(r, v) {
70917
72065
 
70918
72066
  // ../../packages/engine/src/onboard.ts
70919
72067
  init_cjs_shims();
72068
+ function tokenize2(text2) {
72069
+ const withSpaces = text2.replace(/[^a-zA-Z0-9/_.-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
72070
+ const parts = withSpaces.toLowerCase().split(/[\s/_.-]+/).filter((t) => t.length > 0);
72071
+ return [...new Set(parts)];
72072
+ }
72073
+ var GENERIC_PATH_TOKENS = /* @__PURE__ */ new Set([
72074
+ "src",
72075
+ "lib",
72076
+ "app",
72077
+ "test",
72078
+ "tests",
72079
+ "spec",
72080
+ "dist",
72081
+ "node",
72082
+ "modules",
72083
+ "common",
72084
+ "shared",
72085
+ "public",
72086
+ "private",
72087
+ "types",
72088
+ "interfaces",
72089
+ "constants",
72090
+ "config",
72091
+ "index",
72092
+ "main",
72093
+ "server",
72094
+ "client",
72095
+ "api",
72096
+ "http",
72097
+ "db",
72098
+ "database",
72099
+ "model",
72100
+ "models",
72101
+ "schema",
72102
+ "migration",
72103
+ "seed",
72104
+ "fixture",
72105
+ "mock",
72106
+ "hook",
72107
+ "context",
72108
+ "provider",
72109
+ "factory",
72110
+ "middleware",
72111
+ "route",
72112
+ "router",
72113
+ "routes",
72114
+ "service",
72115
+ "services",
72116
+ "controller",
72117
+ "controllers",
72118
+ "resolver",
72119
+ "resolvers",
72120
+ "worker",
72121
+ "workers",
72122
+ "helper",
72123
+ "helpers",
72124
+ "util",
72125
+ "utils"
72126
+ ]);
72127
+ function buildAreaTokens(area, symbols) {
72128
+ const tokens = new Set(tokenize2(area));
72129
+ for (const sym of symbols) {
72130
+ for (const t of tokenize2(sym.name)) {
72131
+ if (t.length >= 3 && !GENERIC_PATH_TOKENS.has(t)) tokens.add(t);
72132
+ }
72133
+ for (const t of tokenize2(sym.filePath)) {
72134
+ if (t.length >= 3 && !GENERIC_PATH_TOKENS.has(t)) tokens.add(t);
72135
+ }
72136
+ }
72137
+ return tokens;
72138
+ }
72139
+ function matchesArea(text2, tokens) {
72140
+ const textTokens = new Set(tokenize2(text2));
72141
+ for (const t of tokens) {
72142
+ if (textTokens.has(t)) return true;
72143
+ }
72144
+ return false;
72145
+ }
72146
+ function areaMatchScore(symbol, tokens) {
72147
+ let score = 0;
72148
+ const nameTokens = tokenize2(symbol.name);
72149
+ const pathTokens = tokenize2(symbol.filePath);
72150
+ for (const t of tokens) {
72151
+ if (nameTokens.includes(t)) score += 2;
72152
+ if (pathTokens.includes(t)) score += 1;
72153
+ }
72154
+ return score;
72155
+ }
72156
+ function bestAreaSymbol(area, symbols) {
72157
+ if (symbols.length === 0) return null;
72158
+ const areaOnlyTokens = new Set(tokenize2(area));
72159
+ let best = null;
72160
+ let bestScore = -1;
72161
+ for (const sym of symbols) {
72162
+ const score = areaMatchScore(sym, areaOnlyTokens);
72163
+ if (score > bestScore) {
72164
+ bestScore = score;
72165
+ best = sym;
72166
+ }
72167
+ }
72168
+ return best;
72169
+ }
72170
+ function filterArchitecture(architecture, tokens) {
72171
+ return {
72172
+ ...architecture,
72173
+ subsystems: architecture.subsystems.filter((s) => matchesArea(s.name, tokens)),
72174
+ asyncBoundaries: architecture.asyncBoundaries.filter(
72175
+ (b2) => matchesArea(b2.queueName, tokens) || b2.producers.some((p) => matchesArea(p, tokens)) || b2.workers.some((w) => matchesArea(w, tokens))
72176
+ ),
72177
+ keyFlows: architecture.keyFlows.filter((f) => matchesArea(f, tokens)),
72178
+ externalSystems: architecture.externalSystems.filter((e) => matchesArea(e.name, tokens))
72179
+ };
72180
+ }
70920
72181
  async function buildOnboarding(input, deps) {
70921
72182
  const architecture = await discoverArchitecture({ code: deps.code, db: deps.db });
70922
- const invs = await listInvestigations(deps.db, 8);
70923
- const pastIncidents = invs.map((i) => ({
70924
- id: i.id,
70925
- title: i.title,
70926
- createdAt: i.createdAt != null ? new Date(i.createdAt).toISOString() : null
70927
- }));
70928
- const ownership = input.area != null ? await estimateOwnership(input.area, { code: deps.code, repoPath: deps.repoPath }) : null;
70929
- const largestName = architecture.subsystems[0]?.name ?? "n/a";
70930
- const summary = (input.area != null ? 'Onboarding for "' + input.area + '": ' : "System onboarding: ") + architecture.subsystems.length + " subsystems (largest " + largestName + "), " + architecture.asyncBoundaries.length + " async queue boundaries, " + architecture.externalSystems.length + " external systems, " + pastIncidents.length + " past investigation(s) on record.";
72183
+ const area = input.area?.trim();
72184
+ let filteredArchitecture = architecture;
72185
+ let pastIncidents = [];
72186
+ let areaSymbol = null;
72187
+ if (area != null && area !== "") {
72188
+ const symbols = await deps.code.searchSymbols(area, 20);
72189
+ const tokens = buildAreaTokens(area, symbols);
72190
+ areaSymbol = bestAreaSymbol(area, symbols);
72191
+ filteredArchitecture = filterArchitecture(architecture, tokens);
72192
+ const invs = await listInvestigationsWithReports(deps.db, 50);
72193
+ const areaTokenArray = [...tokens];
72194
+ const seenIds = /* @__PURE__ */ new Set();
72195
+ for (const inv of invs) {
72196
+ if (!inv.report || seenIds.has(inv.id)) continue;
72197
+ seenIds.add(inv.id);
72198
+ let relevant = false;
72199
+ try {
72200
+ const report = inv.report;
72201
+ const tags = deriveTags(report);
72202
+ const tagSet = new Set(tags.map((t) => t.toLowerCase()));
72203
+ relevant = areaTokenArray.some((t) => tagSet.has(t.toLowerCase()));
72204
+ } catch {
72205
+ relevant = false;
72206
+ }
72207
+ if (!relevant && inv.title != null) {
72208
+ relevant = matchesArea(inv.title, tokens);
72209
+ }
72210
+ if (relevant) {
72211
+ pastIncidents.push({
72212
+ id: inv.id,
72213
+ title: inv.title,
72214
+ createdAt: inv.createdAt != null ? new Date(inv.createdAt).toISOString() : null
72215
+ });
72216
+ }
72217
+ }
72218
+ pastIncidents = pastIncidents.slice(0, 8);
72219
+ } else {
72220
+ const invs = await listInvestigations(deps.db, 8);
72221
+ pastIncidents = invs.map((i) => ({
72222
+ id: i.id,
72223
+ title: i.title,
72224
+ createdAt: i.createdAt != null ? new Date(i.createdAt).toISOString() : null
72225
+ }));
72226
+ }
72227
+ const ownership = area != null && area !== "" ? await estimateOwnership(area, {
72228
+ code: deps.code,
72229
+ repoPath: deps.repoPath,
72230
+ symbol: areaSymbol
72231
+ }) : null;
72232
+ const largestName = filteredArchitecture.subsystems[0]?.name ?? "n/a";
72233
+ const summary = (area != null ? 'Onboarding for "' + area + '": ' : "System onboarding: ") + filteredArchitecture.subsystems.length + " subsystems (largest " + largestName + "), " + filteredArchitecture.asyncBoundaries.length + " async queue boundaries, " + filteredArchitecture.externalSystems.length + " external systems, " + pastIncidents.length + " past investigation(s) on record." + (area != null ? ' Filtered toward "' + area + '".' : "");
70931
72234
  return {
70932
- area: input.area ?? null,
70933
- architecture,
72235
+ area: area ?? null,
72236
+ architecture: filteredArchitecture,
70934
72237
  ownership,
70935
72238
  pastIncidents,
70936
72239
  summary
@@ -70989,7 +72292,7 @@ function renderOnboarding(g) {
70989
72292
  lines.push("");
70990
72293
  lines.push("## What usually breaks");
70991
72294
  lines.push("");
70992
- lines.push(`- Dead-code symbols: ${g.architecture.fragile.deadCode}`);
72295
+ lines.push(`- Unreferenced symbols: ${g.architecture.fragile.deadCode}`);
70993
72296
  lines.push(
70994
72297
  `- High-coupling pairs (co-changes \u2265 3): ${g.architecture.fragile.highCouplingPairs}`
70995
72298
  );
@@ -71083,6 +72386,7 @@ var SCENARIOS = [
71083
72386
  since: "HEAD~10",
71084
72387
  expectedSignals: [
71085
72388
  { key: "seed", label: "Seed symbols resolved" },
72389
+ { key: "commit", label: "Recent change evidence found" },
71086
72390
  { key: "hyp:deployment-regression", label: "deployment-regression hypothesis present" },
71087
72391
  { key: "actions", label: "Next actions generated" }
71088
72392
  ],
@@ -71138,6 +72442,8 @@ function evaluateScenario(scenario, report) {
71138
72442
  ok = report.gapAnalysis.gaps.length > 0;
71139
72443
  } else if (signal.key === "actions") {
71140
72444
  ok = report.nextActions.length > 0;
72445
+ } else if (signal.key === "commit") {
72446
+ ok = report.evidence.some((e) => e.kind === "commit");
71141
72447
  } else if (signal.key.startsWith("hyp:")) {
71142
72448
  const category = signal.key.slice(4);
71143
72449
  ok = report.hypotheses.some((h) => h.category === category);
@@ -71178,6 +72484,16 @@ function renderSimulation(scenario, report, evaluation) {
71178
72484
  "Form your own hypothesis before reading on \u2014 then compare it with what Horus found."
71179
72485
  );
71180
72486
  lines.push("");
72487
+ const isWeak = evaluation.passed < evaluation.total;
72488
+ if (isWeak) {
72489
+ lines.push(
72490
+ "> **Weak investigation** \u2014 Horus did not surface all expected signals for this scenario."
72491
+ );
72492
+ lines.push(
72493
+ "> This can happen when the hint resolves to a symbol that is not directly connected to the expected runtime/change evidence."
72494
+ );
72495
+ lines.push("");
72496
+ }
71181
72497
  lines.push("## Horus investigation");
71182
72498
  lines.push("");
71183
72499
  lines.push(report.summary);
@@ -71209,6 +72525,28 @@ function renderSimulation(scenario, report, evaluation) {
71209
72525
  for (const tip of scenario.coachingTips) {
71210
72526
  lines.push(`- ${tip}`);
71211
72527
  }
72528
+ if (scenario.category === "queue") {
72529
+ const queueBoundaryCheck = evaluation.checks.find(
72530
+ (c) => c.label === "Queue boundary crossing detected"
72531
+ );
72532
+ if (queueBoundaryCheck && !queueBoundaryCheck.ok) {
72533
+ lines.push("");
72534
+ lines.push(
72535
+ "_No queue boundary was detected. The hint likely resolved to a symbol that is not directly connected to a known queue producer or worker. Try re-running with a hint that names a queue worker, producer, or the queue itself._"
72536
+ );
72537
+ }
72538
+ }
72539
+ if (scenario.category === "change") {
72540
+ const commitCheck = evaluation.checks.find(
72541
+ (c) => c.label === "Recent change evidence found"
72542
+ );
72543
+ if (commitCheck && !commitCheck.ok) {
72544
+ lines.push("");
72545
+ lines.push(
72546
+ "_No recent change evidence was found. The deployment-regression scenario needs a diffable git range (`--since`) with commits touching the resolved symbol. Try a more specific hint or a different `--since` range._"
72547
+ );
72548
+ }
72549
+ }
71212
72550
  return lines.join("\n");
71213
72551
  }
71214
72552
 
@@ -71380,6 +72718,54 @@ function validateNarrative(output, input) {
71380
72718
  }
71381
72719
  }
71382
72720
  }
72721
+ if (output.hypothesisJudgments !== void 0) {
72722
+ const knownHypIds = new Set(input.hypotheses?.map((h) => h.id) ?? []);
72723
+ const validVerdicts = /* @__PURE__ */ new Set(["supported", "weakened", "eliminated", "unconfirmed"]);
72724
+ for (const j of output.hypothesisJudgments) {
72725
+ if (input.hypotheses && !knownHypIds.has(j.hypothesisId)) {
72726
+ errors2.push(
72727
+ `hypothesisJudgment references unknown hypothesis ID "${j.hypothesisId}"`
72728
+ );
72729
+ }
72730
+ if (!validVerdicts.has(j.verdict)) {
72731
+ errors2.push(
72732
+ `hypothesisJudgment "${j.hypothesisId}" has invalid verdict "${j.verdict}"`
72733
+ );
72734
+ }
72735
+ if (!j.rationale || j.rationale.trim().length === 0) {
72736
+ errors2.push(`hypothesisJudgment "${j.hypothesisId}" has empty rationale`);
72737
+ }
72738
+ for (const eid of j.citedEvidenceIds) {
72739
+ if (!knownIds.has(eid)) {
72740
+ errors2.push(
72741
+ `hypothesisJudgment "${j.hypothesisId}" cites unknown evidence ID "${eid}"`
72742
+ );
72743
+ }
72744
+ }
72745
+ if (j.confidence < 0 || j.confidence > 1) {
72746
+ errors2.push(
72747
+ `hypothesisJudgment "${j.hypothesisId}" confidence must be between 0 and 1`
72748
+ );
72749
+ }
72750
+ }
72751
+ }
72752
+ if (output.rootCauseAssessment !== void 0) {
72753
+ const rca = output.rootCauseAssessment;
72754
+ if (!rca.summary || rca.summary.trim().length === 0) {
72755
+ errors2.push("rootCauseAssessment.summary must be non-empty");
72756
+ }
72757
+ for (const eid of rca.citedEvidenceIds) {
72758
+ if (!knownIds.has(eid)) {
72759
+ errors2.push(`rootCauseAssessment cites unknown evidence ID "${eid}"`);
72760
+ }
72761
+ }
72762
+ const validUncertainty = /* @__PURE__ */ new Set(["low", "medium", "high"]);
72763
+ if (!validUncertainty.has(rca.uncertainty)) {
72764
+ errors2.push(
72765
+ `rootCauseAssessment.uncertainty must be 'low', 'medium', or 'high' (got "${rca.uncertainty}")`
72766
+ );
72767
+ }
72768
+ }
71383
72769
  return { valid: errors2.length === 0, errors: errors2 };
71384
72770
  }
71385
72771
  async function renderNarrative(input, opts = {}) {
@@ -71397,11 +72783,11 @@ async function renderNarrative(input, opts = {}) {
71397
72783
  fromProvider: false,
71398
72784
  validationErrors: validation.errors
71399
72785
  };
71400
- } catch {
72786
+ } catch (err) {
71401
72787
  return {
71402
72788
  output: deterministicFallback(input),
71403
72789
  fromProvider: false,
71404
- validationErrors: ["Provider threw an error"]
72790
+ validationErrors: [err instanceof Error ? err.message : "Provider threw an error"]
71405
72791
  };
71406
72792
  }
71407
72793
  }
@@ -71468,7 +72854,26 @@ function buildPrompt(input, ceiling) {
71468
72854
  (e) => `- [${e.id}] (${e.kind}) ${e.title}${e.excerpt ? `: ${e.excerpt}` : ""}`
71469
72855
  ).join("\n");
71470
72856
  const causeLines = input.suspectedCauses.map((c) => `- ${c.label} (score: ${c.score})`).join("\n");
71471
- return `You are an incident analysis assistant. Analyze this investigation and return a JSON narrative.
72857
+ const hypothesisLines = input.hypotheses && input.hypotheses.length > 0 ? input.hypotheses.map(
72858
+ (h) => `- [${h.id}] (${h.category}) [deterministic: ${h.deterministicVerdict} @ ${h.deterministicConfidence.toFixed(2)}] ${h.statement}`
72859
+ ).join("\n") : null;
72860
+ const hypothesisJudgmentSchema = hypothesisLines ? ` "hypothesisJudgments": [
72861
+ {
72862
+ "hypothesisId": "<id from hypotheses list above>",
72863
+ "category": "<category from hypotheses list>",
72864
+ "verdict": "<supported|weakened|eliminated|unconfirmed>",
72865
+ "rationale": "<one paragraph grounded in evidence IDs>",
72866
+ "citedEvidenceIds": ["<evidence IDs from the list>"],
72867
+ "confidence": <0\u2013${ceiling}>
72868
+ }
72869
+ ],
72870
+ "rootCauseAssessment": {
72871
+ "summary": "<one paragraph root cause grounded in evidence>",
72872
+ "primaryHypothesisId": "<id of the hypothesis you consider the primary driver, or omit>",
72873
+ "citedEvidenceIds": ["<evidence IDs from the list>"],
72874
+ "uncertainty": "<low|medium|high>"
72875
+ },` : "";
72876
+ return `You are an incident analysis assistant. Analyze this investigation and return a JSON judgment.
71472
72877
 
71473
72878
  Investigation hint: ${input.hint}
71474
72879
  Deterministic summary: ${input.deterministicSummary}
@@ -71478,23 +72883,30 @@ ${evidenceLines}
71478
72883
 
71479
72884
  Suspected causes:
71480
72885
  ${causeLines}
72886
+ ${hypothesisLines ? `
72887
+ Deterministic hypotheses (provide a second-pass judgment for each):
72888
+ ${hypothesisLines}` : ""}
71481
72889
 
71482
72890
  Known services: ${input.knownServices.join(", ")}
71483
72891
 
71484
72892
  Return ONLY valid JSON with this exact shape:
71485
72893
  {
71486
72894
  "what": "<what happened \u2014 one concise paragraph>",
71487
- "why": "<root cause analysis grounded in the evidence above>",
72895
+ "why": "<root cause narrative grounded in the evidence above>",
71488
72896
  "whereNext": ["<action 1>", "<action 2>"],
71489
72897
  "citations": [{"evidenceId": "<id from the evidence list>", "rationale": "<why this supports the claim>"}],
71490
72898
  "confidence": <number between 0 and ${ceiling}>,
71491
- "mentionedServices": ["<service name from known list only>"]
72899
+ "mentionedServices": ["<service name from known list only>"],
72900
+ ${hypothesisJudgmentSchema}
71492
72901
  }
71493
72902
 
71494
72903
  Hard rules:
71495
72904
  - confidence must not exceed ${ceiling}
71496
72905
  - only cite evidence IDs from the list above \u2014 any other ID is a hallucination
71497
- - only include services from: ${input.knownServices.join(", ")}`;
72906
+ - only include services from: ${input.knownServices.join(", ")}
72907
+ - hypothesisJudgments must only reference hypothesis IDs from the hypotheses list above
72908
+ - verdict must be exactly one of: supported, weakened, eliminated, unconfirmed
72909
+ - uncertainty must be exactly one of: low, medium, high`;
71498
72910
  }
71499
72911
  function parseOutput(raw, input, ceiling) {
71500
72912
  let parsed;
@@ -71508,21 +72920,52 @@ function parseOutput(raw, input, ceiling) {
71508
72920
  parsed = JSON.parse(jsonMatch[0]);
71509
72921
  }
71510
72922
  const confidence = Math.min(
71511
- typeof parsed.confidence === "number" ? parsed.confidence : input.reportConfidence,
72923
+ typeof parsed["confidence"] === "number" ? parsed["confidence"] : input.reportConfidence,
71512
72924
  ceiling
71513
72925
  );
72926
+ const citations = Array.isArray(parsed["citations"]) ? parsed["citations"] : [];
71514
72927
  const output = {
71515
- what: typeof parsed.what === "string" ? parsed.what : "",
71516
- why: typeof parsed.why === "string" ? parsed.why : "",
71517
- whereNext: Array.isArray(parsed.whereNext) ? parsed.whereNext.map(String) : [],
71518
- citations: Array.isArray(parsed.citations) ? parsed.citations : [],
72928
+ what: typeof parsed["what"] === "string" ? parsed["what"] : "",
72929
+ why: typeof parsed["why"] === "string" ? parsed["why"] : "",
72930
+ whereNext: Array.isArray(parsed["whereNext"]) ? parsed["whereNext"].map(String) : [],
72931
+ citations,
71519
72932
  confidence
71520
72933
  };
71521
- if (Array.isArray(parsed.mentionedServices)) {
71522
- output.mentionedServices = parsed.mentionedServices.map(String);
72934
+ if (Array.isArray(parsed["mentionedServices"])) {
72935
+ output.mentionedServices = parsed["mentionedServices"].map(String);
72936
+ }
72937
+ if (Array.isArray(parsed["hypothesisJudgments"])) {
72938
+ output.hypothesisJudgments = parsed["hypothesisJudgments"].map(
72939
+ (j) => ({
72940
+ hypothesisId: typeof j["hypothesisId"] === "string" ? j["hypothesisId"] : "",
72941
+ category: typeof j["category"] === "string" ? j["category"] : "",
72942
+ verdict: isValidVerdict(j["verdict"]) ? j["verdict"] : "unconfirmed",
72943
+ rationale: typeof j["rationale"] === "string" ? j["rationale"] : "",
72944
+ citedEvidenceIds: Array.isArray(j["citedEvidenceIds"]) ? j["citedEvidenceIds"].map(String) : [],
72945
+ confidence: Math.min(
72946
+ typeof j["confidence"] === "number" ? j["confidence"] : 0,
72947
+ ceiling
72948
+ )
72949
+ })
72950
+ );
72951
+ }
72952
+ if (parsed["rootCauseAssessment"] && typeof parsed["rootCauseAssessment"] === "object") {
72953
+ const rca = parsed["rootCauseAssessment"];
72954
+ output.rootCauseAssessment = {
72955
+ summary: typeof rca["summary"] === "string" ? rca["summary"] : "",
72956
+ primaryHypothesisId: typeof rca["primaryHypothesisId"] === "string" ? rca["primaryHypothesisId"] : void 0,
72957
+ citedEvidenceIds: Array.isArray(rca["citedEvidenceIds"]) ? rca["citedEvidenceIds"].map(String) : [],
72958
+ uncertainty: isValidUncertainty(rca["uncertainty"]) ? rca["uncertainty"] : "high"
72959
+ };
71523
72960
  }
71524
72961
  return output;
71525
72962
  }
72963
+ function isValidVerdict(v) {
72964
+ return v === "supported" || v === "weakened" || v === "eliminated" || v === "unconfirmed";
72965
+ }
72966
+ function isValidUncertainty(v) {
72967
+ return v === "low" || v === "medium" || v === "high";
72968
+ }
71526
72969
 
71527
72970
  // ../../packages/ai/src/fake-provider.ts
71528
72971
  init_cjs_shims();
@@ -71562,6 +73005,12 @@ init_cjs_shims();
71562
73005
  init_cjs_shims();
71563
73006
 
71564
73007
  // ../../packages/cli/src/commands/investigate.ts
73008
+ function extractEvidenceExcerpt(e) {
73009
+ if (!e.payload || typeof e.payload !== "object") return void 0;
73010
+ const p = e.payload;
73011
+ const candidate = (typeof p["pattern"] === "string" ? p["pattern"] : null) ?? (typeof p["message"] === "string" ? p["message"] : null) ?? (typeof p["label"] === "string" ? p["label"] : null) ?? (typeof p["summary"] === "string" ? p["summary"] : null) ?? (typeof p["valueLabel"] === "string" ? p["valueLabel"] : null);
73012
+ return candidate ? candidate.slice(0, 120) : void 0;
73013
+ }
71565
73014
  function buildNarrativeInput(report) {
71566
73015
  return {
71567
73016
  investigationId: report.id,
@@ -71570,7 +73019,8 @@ function buildNarrativeInput(report) {
71570
73019
  evidence: report.evidence.map((e) => ({
71571
73020
  id: e.id,
71572
73021
  kind: e.kind,
71573
- title: e.title
73022
+ title: e.title,
73023
+ excerpt: extractEvidenceExcerpt(e)
71574
73024
  })),
71575
73025
  knownServices: report.input.service ? [report.input.service] : [],
71576
73026
  suspectedCauses: report.suspectedCauses.map((c) => ({
@@ -71582,9 +73032,83 @@ function buildNarrativeInput(report) {
71582
73032
  findings: report.findings.map((f) => ({
71583
73033
  title: f.title,
71584
73034
  evidenceIds: f.evidenceIds
73035
+ })),
73036
+ hypotheses: report.hypotheses.map((h) => ({
73037
+ id: h.id,
73038
+ category: h.category,
73039
+ statement: h.statement,
73040
+ deterministicVerdict: h.verdict,
73041
+ deterministicConfidence: h.confidence,
73042
+ supportingEvidenceIds: h.supportingEvidenceIds
71585
73043
  }))
71586
73044
  };
71587
73045
  }
73046
+ function narrativeOutputToStoredJudgment(output, provider) {
73047
+ const judgment = {
73048
+ what: output.what,
73049
+ why: output.why,
73050
+ whereNext: output.whereNext,
73051
+ citations: output.citations,
73052
+ confidence: output.confidence,
73053
+ provider,
73054
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
73055
+ };
73056
+ if (output.mentionedServices) judgment.mentionedServices = output.mentionedServices;
73057
+ if (output.hypothesisJudgments) judgment.hypothesisJudgments = output.hypothesisJudgments;
73058
+ if (output.rootCauseAssessment) judgment.rootCauseAssessment = output.rootCauseAssessment;
73059
+ return judgment;
73060
+ }
73061
+ function renderStoredAIJudgment(judgment, write = (l) => console.log(l)) {
73062
+ const sep = "\u2500".repeat(60);
73063
+ write(`
73064
+ ${sep}`);
73065
+ write(import_picocolors5.default.bold("AI Judgment") + import_picocolors5.default.dim(` (confidence: ${(judgment.confidence * 100).toFixed(0)}%, provider: ${judgment.provider})`));
73066
+ write(sep);
73067
+ if (judgment.hypothesisJudgments && judgment.hypothesisJudgments.length > 0) {
73068
+ write(import_picocolors5.default.bold("Hypothesis verdicts:"));
73069
+ for (const j of judgment.hypothesisJudgments) {
73070
+ const verdictColor = j.verdict === "supported" ? import_picocolors5.default.green : j.verdict === "weakened" ? import_picocolors5.default.yellow : j.verdict === "eliminated" ? import_picocolors5.default.red : import_picocolors5.default.dim;
73071
+ write(` ${verdictColor(`[${j.verdict}]`)} ${j.category}` + import_picocolors5.default.dim(` (${(j.confidence * 100).toFixed(0)}%)`));
73072
+ write(import_picocolors5.default.dim(` ${j.rationale}`));
73073
+ }
73074
+ write("");
73075
+ }
73076
+ if (judgment.rootCauseAssessment) {
73077
+ const rca = judgment.rootCauseAssessment;
73078
+ const uncertaintyColor = rca.uncertainty === "low" ? import_picocolors5.default.green : rca.uncertainty === "medium" ? import_picocolors5.default.yellow : import_picocolors5.default.red;
73079
+ write(import_picocolors5.default.bold("Root cause assessment:") + import_picocolors5.default.dim(` uncertainty: ${uncertaintyColor(rca.uncertainty)}`));
73080
+ write(rca.summary);
73081
+ if (rca.citedEvidenceIds.length > 0) {
73082
+ write(import_picocolors5.default.dim(` Cited: ${rca.citedEvidenceIds.join(", ")}`));
73083
+ }
73084
+ write("");
73085
+ }
73086
+ write(import_picocolors5.default.bold("What:") + " " + judgment.what);
73087
+ write(import_picocolors5.default.bold("Why:") + " " + judgment.why);
73088
+ if (judgment.whereNext.length > 0) {
73089
+ write(import_picocolors5.default.bold("Next steps:"));
73090
+ for (const step of judgment.whereNext) {
73091
+ write(` \u2022 ${step}`);
73092
+ }
73093
+ }
73094
+ if (judgment.citations.length > 0) {
73095
+ write(import_picocolors5.default.dim(`
73096
+ Cited evidence: ${judgment.citations.map((c) => c.evidenceId).join(", ")}`));
73097
+ }
73098
+ }
73099
+ function classifyAIFailure(firstError) {
73100
+ if (!firstError) return "provider unavailable";
73101
+ if (/401|unauthorized|api.key|api key/i.test(firstError)) {
73102
+ return "missing or invalid API key \u2014 set ANTHROPIC_API_KEY";
73103
+ }
73104
+ if (/invalid.request|model|not.found/i.test(firstError)) {
73105
+ return `invalid model or request \u2014 ${firstError}`;
73106
+ }
73107
+ if (/econnrefused|enotfound|network|fetch|abort/i.test(firstError)) {
73108
+ return "network error \u2014 check connectivity";
73109
+ }
73110
+ return firstError;
73111
+ }
71588
73112
  async function runInvestigate(hint, opts) {
71589
73113
  try {
71590
73114
  const config = await loadConfig(opts.config, { name: opts.name });
@@ -71644,33 +73168,22 @@ async function runInvestigate(hint, opts) {
71644
73168
  const rendered = format === "json" ? reportToJSON(report) : format === "markdown" || format === "md" ? reportToMarkdown(report) : renderReport2(report);
71645
73169
  console.log(rendered);
71646
73170
  if (opts.ai && format !== "json") {
73171
+ const model = opts.aiModel ?? "claude-opus-4-8";
73172
+ const provider = opts._aiProvider ?? new AnthropicNarrativeProvider({ model });
73173
+ console.log(import_picocolors5.default.dim(`[ai] model: ${model}`));
71647
73174
  const narrativeInput = buildNarrativeInput(report);
71648
- const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
71649
73175
  const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
71650
73176
  if (!fromProvider) {
71651
- console.error(import_picocolors5.default.yellow(`[ai] Provider unavailable \u2014 deterministic output shown above.`));
71652
- if (validationErrors?.length) {
71653
- console.error(import_picocolors5.default.dim(` ${validationErrors[0]}`));
71654
- }
73177
+ const reason = classifyAIFailure(validationErrors?.[0]);
73178
+ console.error(import_picocolors5.default.yellow(`[ai] fallback to deterministic \u2014 ${reason}`));
71655
73179
  } else {
71656
- const sep = "\u2500".repeat(60);
71657
- console.log(`
71658
- ${sep}`);
71659
- console.log(import_picocolors5.default.bold("AI Narrative"));
71660
- console.log(sep);
71661
- console.log(import_picocolors5.default.bold("What:"), output.what);
71662
- console.log(import_picocolors5.default.bold("Why:"), output.why);
71663
- if (output.whereNext.length > 0) {
71664
- console.log(import_picocolors5.default.bold("Next steps:"));
71665
- for (const step of output.whereNext) {
71666
- console.log(` \u2022 ${step}`);
71667
- }
71668
- }
71669
- if (output.citations.length > 0) {
71670
- console.log(import_picocolors5.default.dim(`
71671
- Cited evidence: ${output.citations.map((c) => c.evidenceId).join(", ")}`));
73180
+ const stored = narrativeOutputToStoredJudgment(output, "anthropic");
73181
+ report.aiJudgment = stored;
73182
+ try {
73183
+ await updateInvestigationReport(db, report.id, report);
73184
+ } catch {
71672
73185
  }
71673
- console.log(import_picocolors5.default.dim(`AI confidence: ${(output.confidence * 100).toFixed(0)}%`));
73186
+ renderStoredAIJudgment(stored);
71674
73187
  }
71675
73188
  }
71676
73189
  } finally {
@@ -71806,9 +73319,15 @@ async function runBlastRadius(query, opts) {
71806
73319
  try {
71807
73320
  const r = await analyzeBlastRadius(query, { code, db }, opts.depth ?? 3);
71808
73321
  if (!r) {
71809
- console.log("No symbol found for: " + query);
73322
+ console.log(`No symbol found for: ${query}`);
73323
+ console.log(import_picocolors10.default.dim(` Tip: use an exact class or function name, e.g. "MyService"`));
71810
73324
  return 1;
71811
73325
  }
73326
+ if (r.seed.name.toLowerCase() !== query.toLowerCase()) {
73327
+ console.log(
73328
+ import_picocolors10.default.yellow(` No exact match for "${query}"`) + import_picocolors10.default.dim(` \u2014 showing closest: "${r.seed.name}" (fuzzy match)`)
73329
+ );
73330
+ }
71812
73331
  console.log(opts.json ? blastRadiusToJSON(r) : renderBlastRadius(r));
71813
73332
  } finally {
71814
73333
  await sql2.end();
@@ -71881,6 +73400,26 @@ async function runSearch(query, opts) {
71881
73400
 
71882
73401
  // ../../packages/cli/src/commands/investigations.ts
71883
73402
  init_cjs_shims();
73403
+
73404
+ // ../../packages/cli/src/lib/format.ts
73405
+ init_cjs_shims();
73406
+ function formatDateTime(date2) {
73407
+ if (date2 == null) return "";
73408
+ const d = date2 instanceof Date ? date2 : new Date(date2);
73409
+ if (Number.isNaN(d.getTime())) return String(date2);
73410
+ const tzOffset = -d.getTimezoneOffset();
73411
+ const sign = tzOffset >= 0 ? "+" : "-";
73412
+ const offsetHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, "0");
73413
+ const offsetMins = String(Math.abs(tzOffset) % 60).padStart(2, "0");
73414
+ const y = String(d.getFullYear());
73415
+ const m = String(d.getMonth() + 1).padStart(2, "0");
73416
+ const day = String(d.getDate()).padStart(2, "0");
73417
+ const h = String(d.getHours()).padStart(2, "0");
73418
+ const min = String(d.getMinutes()).padStart(2, "0");
73419
+ return `${y}-${m}-${day} ${h}:${min} (UTC${sign}${offsetHours}:${offsetMins})`;
73420
+ }
73421
+
73422
+ // ../../packages/cli/src/commands/investigations.ts
71884
73423
  async function runInvestigations(opts) {
71885
73424
  const config = await loadConfig(opts.config);
71886
73425
  const { db, sql: sql2 } = createDb(config.database.url);
@@ -71890,7 +73429,7 @@ async function runInvestigations(opts) {
71890
73429
  console.log('No investigations yet. Run: horus investigate "<hint>"');
71891
73430
  } else {
71892
73431
  for (const row of rows) {
71893
- const ts = row.createdAt.toISOString();
73432
+ const ts = formatDateTime(row.createdAt);
71894
73433
  const title = (row.title ?? "").length > 60 ? (row.title ?? "").slice(0, 57) + "..." : row.title ?? "";
71895
73434
  console.log(`${row.id} ${ts} ${title}`);
71896
73435
  }
@@ -71926,33 +73465,27 @@ async function runReplay(id, opts) {
71926
73465
  const out = fmt === "json" ? reportToJSON(report) : fmt === "markdown" || fmt === "md" ? reportToMarkdown(report) : renderReport2(report);
71927
73466
  console.log(out);
71928
73467
  if (opts.ai && fmt !== "json") {
71929
- const narrativeInput = buildNarrativeInput(report);
71930
- const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
71931
- const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
71932
- if (!fromProvider) {
71933
- console.error(import_picocolors13.default.yellow("[ai] Provider unavailable \u2014 deterministic output shown above."));
71934
- if (validationErrors?.length) {
71935
- console.error(import_picocolors13.default.dim(` ${validationErrors[0]}`));
71936
- }
73468
+ if (report.aiJudgment && !opts.refreshAi) {
73469
+ renderStoredAIJudgment(report.aiJudgment);
73470
+ console.log(import_picocolors13.default.dim("[ai] Stored judgment replayed. Use --refresh-ai to regenerate."));
71937
73471
  } else {
71938
- const sep = "\u2500".repeat(60);
71939
- console.log(`
71940
- ${sep}`);
71941
- console.log(import_picocolors13.default.bold("AI Narrative"));
71942
- console.log(sep);
71943
- console.log(import_picocolors13.default.bold("What:"), output.what);
71944
- console.log(import_picocolors13.default.bold("Why:"), output.why);
71945
- if (output.whereNext.length > 0) {
71946
- console.log(import_picocolors13.default.bold("Next steps:"));
71947
- for (const step of output.whereNext) {
71948
- console.log(` \u2022 ${step}`);
73472
+ const narrativeInput = buildNarrativeInput(report);
73473
+ const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
73474
+ const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
73475
+ if (!fromProvider) {
73476
+ console.error(import_picocolors13.default.yellow("[ai] Provider unavailable \u2014 deterministic output shown above."));
73477
+ if (validationErrors?.length) {
73478
+ console.error(import_picocolors13.default.dim(` ${validationErrors[0]}`));
71949
73479
  }
73480
+ } else {
73481
+ const stored = narrativeOutputToStoredJudgment(output, "anthropic");
73482
+ report.aiJudgment = stored;
73483
+ try {
73484
+ await updateInvestigationReport(db, report.id, report);
73485
+ } catch {
73486
+ }
73487
+ renderStoredAIJudgment(stored);
71950
73488
  }
71951
- if (output.citations.length > 0) {
71952
- console.log(import_picocolors13.default.dim(`
71953
- Cited evidence: ${output.citations.map((c) => c.evidenceId).join(", ")}`));
71954
- }
71955
- console.log(import_picocolors13.default.dim(`AI confidence: ${(output.confidence * 100).toFixed(0)}%`));
71956
73489
  }
71957
73490
  }
71958
73491
  } finally {
@@ -72014,34 +73547,74 @@ async function runPostmortem(id, opts) {
72014
73547
  }
72015
73548
  let content = generatePostmortem(report);
72016
73549
  if (opts.aiSummary) {
72017
- const narrativeInput = buildNarrativeInput(report);
72018
- const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
72019
- const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
72020
- if (!fromProvider) {
72021
- content += `
72022
-
72023
- ## AI Summary
73550
+ const storedJudgment = report.aiJudgment;
73551
+ if (storedJudgment && !opts.refreshAi) {
73552
+ content += "\n\n## AI Summary\n\n";
73553
+ content += `_Stored AI judgment (provider: ${storedJudgment.provider}, generated: ${storedJudgment.generatedAt})_
72024
73554
 
72025
- _AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
72026
73555
  `;
72027
- } else {
72028
- content += "\n\n## AI Summary\n\n";
72029
- content += `**What happened:** ${output.what}
73556
+ content += `**What happened:** ${storedJudgment.what}
72030
73557
 
72031
73558
  `;
72032
- content += `**Why:** ${output.why}
73559
+ content += `**Why:** ${storedJudgment.why}
73560
+ `;
73561
+ if (storedJudgment.rootCauseAssessment) {
73562
+ content += `
73563
+ **Root cause (AI):** ${storedJudgment.rootCauseAssessment.summary}
73564
+ `;
73565
+ content += `_(uncertainty: ${storedJudgment.rootCauseAssessment.uncertainty})_
72033
73566
  `;
72034
- if (output.whereNext.length > 0) {
73567
+ }
73568
+ if (storedJudgment.whereNext.length > 0) {
72035
73569
  content += "\n**Next steps:**\n";
72036
- for (const step of output.whereNext) {
73570
+ for (const step of storedJudgment.whereNext) {
72037
73571
  content += `- ${step}
72038
73572
  `;
72039
73573
  }
72040
73574
  }
72041
- if (output.citations.length > 0) {
73575
+ if (storedJudgment.citations.length > 0) {
73576
+ content += `
73577
+ **Cited evidence:** ${storedJudgment.citations.map((c) => c.evidenceId).join(", ")}
73578
+ `;
73579
+ }
73580
+ } else {
73581
+ const narrativeInput = buildNarrativeInput(report);
73582
+ const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
73583
+ const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
73584
+ if (!fromProvider) {
72042
73585
  content += `
73586
+
73587
+ ## AI Summary
73588
+
73589
+ _AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
73590
+ `;
73591
+ } else {
73592
+ content += "\n\n## AI Summary\n\n";
73593
+ content += `**What happened:** ${output.what}
73594
+
73595
+ `;
73596
+ content += `**Why:** ${output.why}
73597
+ `;
73598
+ if (output.rootCauseAssessment) {
73599
+ content += `
73600
+ **Root cause (AI):** ${output.rootCauseAssessment.summary}
73601
+ `;
73602
+ content += `_(uncertainty: ${output.rootCauseAssessment.uncertainty})_
73603
+ `;
73604
+ }
73605
+ if (output.whereNext.length > 0) {
73606
+ content += "\n**Next steps:**\n";
73607
+ for (const step of output.whereNext) {
73608
+ content += `- ${step}
73609
+ `;
73610
+ }
73611
+ }
73612
+ if (output.citations.length > 0) {
73613
+ content += `
72043
73614
  **Cited evidence:** ${output.citations.map((c) => c.evidenceId).join(", ")}
72044
73615
  `;
73616
+ }
73617
+ report.aiJudgment = narrativeOutputToStoredJudgment(output, "anthropic");
72045
73618
  }
72046
73619
  }
72047
73620
  }
@@ -72114,7 +73687,7 @@ async function runScores(opts) {
72114
73687
  }
72115
73688
  for (const s of scored) {
72116
73689
  console.log(
72117
- " " + String(s.score).padStart(3) + "/100 " + (s.createdAt ? new Date(s.createdAt).toISOString() : "") + " " + s.id + " " + (s.title ?? "")
73690
+ " " + String(s.score).padStart(3) + "/100 " + formatDateTime(s.createdAt) + " " + s.id + " " + (s.title ?? "")
72118
73691
  );
72119
73692
  }
72120
73693
  const avg = Math.round(
@@ -72146,6 +73719,18 @@ async function runAsk(id, directive, opts) {
72146
73719
  const report = migrateReport(row.report);
72147
73720
  const v = refineInvestigation(report, directive);
72148
73721
  console.log(opts.json ? refinedToJSON(report, v) : renderRefined(report, v));
73722
+ if (!opts.json && report.aiJudgment) {
73723
+ const j = report.aiJudgment;
73724
+ console.log("");
73725
+ console.log(import_picocolors17.default.dim("\u2500".repeat(60)));
73726
+ console.log(import_picocolors17.default.dim(`Stored AI judgment (${j.provider}, ${j.generatedAt}):`));
73727
+ if (j.rootCauseAssessment) {
73728
+ console.log(import_picocolors17.default.bold("Root cause (AI):"), j.rootCauseAssessment.summary);
73729
+ console.log(import_picocolors17.default.dim(`Uncertainty: ${j.rootCauseAssessment.uncertainty}`));
73730
+ } else {
73731
+ console.log(import_picocolors17.default.bold("AI Why:"), j.why);
73732
+ }
73733
+ }
72149
73734
  } catch (err) {
72150
73735
  const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
72151
73736
  if (code === "22P02") {
@@ -72290,6 +73875,7 @@ async function runLogs(service, opts) {
72290
73875
  return 1;
72291
73876
  }
72292
73877
  const resolvedService = service ?? renv.connectors.elasticsearch?.serviceName;
73878
+ const indexPattern = renv.connectors.elasticsearch?.indexPattern ?? "*";
72293
73879
  try {
72294
73880
  const compat = await logs.checkCompatibility({
72295
73881
  requiresService: resolvedService !== void 0,
@@ -72315,7 +73901,8 @@ async function runLogs(service, opts) {
72315
73901
  } catch {
72316
73902
  console.warn(import_picocolors20.default.dim("[warn] Elasticsearch compatibility check unavailable \u2014 proceeding."));
72317
73903
  }
72318
- const from = sinceToIso(opts.since);
73904
+ const from = sinceToIso(opts.since) ?? new Date(Date.now() - 7 * 864e5).toISOString();
73905
+ const fromDisplay = from.slice(0, 16).replace("T", " ");
72319
73906
  if (opts.raw === true) {
72320
73907
  const records = await logs.searchLogs({
72321
73908
  service: resolvedService,
@@ -72337,22 +73924,42 @@ async function runLogs(service, opts) {
72337
73924
  }
72338
73925
  return 0;
72339
73926
  }
72340
- const analysis = await logs.analyzeErrors({
73927
+ let analysis = await logs.analyzeErrors({
72341
73928
  service: resolvedService,
72342
73929
  from,
72343
73930
  text: opts.grep
72344
73931
  });
73932
+ let scopeService = resolvedService;
73933
+ let broadeningNote;
73934
+ if (analysis.signatures.length === 0 && resolvedService !== void 0) {
73935
+ const broader = await logs.analyzeErrors({ from, text: opts.grep });
73936
+ if (broader.signatures.length > 0) {
73937
+ analysis = broader;
73938
+ broadeningNote = `No errors found for service "${resolvedService}" \u2014 showing all services`;
73939
+ scopeService = void 0;
73940
+ }
73941
+ }
72345
73942
  console.log(
72346
73943
  import_picocolors20.default.bold(`Error analysis`) + import_picocolors20.default.dim(
72347
- ` \u2014 ${renv.project}/${renv.env}` + (resolvedService ? ` \xB7 service ${resolvedService}` : "") + (opts.grep ? ` \xB7 grep "${opts.grep}"` : "")
73944
+ ` \u2014 ${renv.project}/${renv.env}` + (scopeService ? ` \xB7 service ${scopeService}` : "") + (opts.grep ? ` \xB7 grep "${opts.grep}"` : "")
72348
73945
  )
72349
73946
  );
73947
+ console.log(import_picocolors20.default.dim(` index: ${indexPattern} \xB7 from: ${fromDisplay} UTC \xB7 severity: error+`));
73948
+ if (broadeningNote) {
73949
+ console.log(import_picocolors20.default.yellow(` ${broadeningNote}`));
73950
+ }
72350
73951
  console.log(
72351
73952
  ` ${analysis.totalErrors} error(s) \xB7 ${analysis.signatures.length} signature(s) \xB7 ${import_picocolors20.default.yellow(String(analysis.newSignatures.length))} new`
72352
73953
  );
72353
73954
  console.log("");
72354
73955
  if (analysis.signatures.length === 0) {
72355
73956
  console.log(import_picocolors20.default.dim(" No error-level logs in the window."));
73957
+ console.log(
73958
+ import_picocolors20.default.dim(
73959
+ ` Searched: ${scopeService ? `service "${scopeService}"` : "all services"} \xB7 index: ${indexPattern} \xB7 from: ${fromDisplay} UTC`
73960
+ )
73961
+ );
73962
+ console.log(import_picocolors20.default.dim(` Tip: use --since to widen the window (e.g. --since 30d).`));
72356
73963
  return 0;
72357
73964
  }
72358
73965
  for (const s of analysis.signatures) {
@@ -72477,18 +74084,29 @@ async function runMetrics(hint, opts) {
72477
74084
  const flagged = findings2.filter((f) => f.anomaly !== "none");
72478
74085
  const ok = findings2.filter((f) => f.anomaly === "none");
72479
74086
  if (findings2.length === 0) {
72480
- console.log(
72481
- import_picocolors21.default.dim(
72482
- hint !== void 0 ? `No panels matched hint "${hint}".` : "No panels found in configured Grafana dashboards."
72483
- )
72484
- );
74087
+ if (hint !== void 0) {
74088
+ const tokens = extractHintTokens(hint);
74089
+ console.log(import_picocolors21.default.dim(`No panels matched hint "${hint}".`));
74090
+ if (tokens.length > 0) {
74091
+ console.log(import_picocolors21.default.dim(` Searched for panels containing: ${tokens.join(", ")}`));
74092
+ }
74093
+ console.log(
74094
+ import_picocolors21.default.dim(
74095
+ ` Tip: run ${import_picocolors21.default.bold("horus metrics")} (no hint) to see all available panels, then use a panel title or keyword directly.`
74096
+ )
74097
+ );
74098
+ } else {
74099
+ console.log(import_picocolors21.default.dim("No panels found in configured Grafana dashboards."));
74100
+ }
72485
74101
  return 0;
72486
74102
  }
72487
74103
  const hintSuffix = hint !== void 0 ? ` (hint: "${hint}")` : "";
74104
+ const matchSources = hint !== void 0 ? [...new Set(findings2.map((f) => f.matchSource).filter(Boolean))] : [];
74105
+ const matchSuffix = matchSources.length > 0 ? import_picocolors21.default.dim(` [matched via ${matchSources.join(", ")}]`) : "";
72488
74106
  console.log(
72489
74107
  import_picocolors21.default.bold(
72490
74108
  `Grafana metrics${hintSuffix} \u2014 ${findings2.length} series across panels`
72491
- )
74109
+ ) + matchSuffix
72492
74110
  );
72493
74111
  console.log(
72494
74112
  import_picocolors21.default.dim(
@@ -72555,8 +74173,9 @@ async function runState(opts) {
72555
74173
  const analysis = await mongo.analyzeState(
72556
74174
  staleHours !== void 0 ? { staleHours } : {}
72557
74175
  );
74176
+ const discoveryNote = analysis.autoDiscovered ? import_picocolors22.default.dim(` (${analysis.collections.length} collections, auto-discovered)`) : "";
72558
74177
  console.log(
72559
- import_picocolors22.default.bold("State analysis") + import_picocolors22.default.dim(` \u2014 ${renv.project}/${renv.env} \xB7 db ${analysis.database}`)
74178
+ import_picocolors22.default.bold("State analysis") + import_picocolors22.default.dim(` \u2014 ${renv.project}/${renv.env} \xB7 db ${analysis.database}`) + discoveryNote
72560
74179
  );
72561
74180
  console.log("");
72562
74181
  let signals = 0;
@@ -72574,9 +74193,23 @@ async function runState(opts) {
72574
74193
  console.log(flags.length > 0 ? `${head} ${flags.join(" ")}` : import_picocolors22.default.dim(head));
72575
74194
  }
72576
74195
  console.log("");
72577
- console.log(
72578
- signals > 0 ? ` ${import_picocolors22.default.bold(String(signals))} state signal(s) across ${analysis.collections.length} allowlisted collection(s)` : import_picocolors22.default.dim(` No state anomalies across ${analysis.collections.length} collection(s).`)
72579
- );
74196
+ if (analysis.collections.length === 0) {
74197
+ console.log(
74198
+ import_picocolors22.default.yellow(" No collections discovered in this database.") + import_picocolors22.default.dim(" Verify the database name in your MongoDB connector config.")
74199
+ );
74200
+ } else {
74201
+ const scope = analysis.autoDiscovered ? "collection(s)" : "allowlisted collection(s)";
74202
+ console.log(
74203
+ signals > 0 ? ` ${import_picocolors22.default.bold(String(signals))} state signal(s) across ${analysis.collections.length} ${scope}` : import_picocolors22.default.dim(` No state anomalies across ${analysis.collections.length} ${scope}.`)
74204
+ );
74205
+ if (analysis.autoDiscovered) {
74206
+ console.log(
74207
+ import_picocolors22.default.dim(
74208
+ ` Tip: add a "collections" list to your MongoDB connector config to restrict analysis.`
74209
+ )
74210
+ );
74211
+ }
74212
+ }
72580
74213
  return 0;
72581
74214
  } finally {
72582
74215
  await mongo.close();
@@ -72669,8 +74302,7 @@ async function runSetup(opts) {
72669
74302
  write(
72670
74303
  import_picocolors25.default.dim(
72671
74304
  ` install it (Python 3.11+ required):
72672
- uv tool install axoniq==${PINNED_SOURCE_VERSION}
72673
- or: pip install axoniq==${PINNED_SOURCE_VERSION}
74305
+ pip install horus-source
72674
74306
  ensure ~/.local/bin is on your PATH`
72675
74307
  )
72676
74308
  );
@@ -72682,8 +74314,7 @@ async function runSetup(opts) {
72682
74314
  write(
72683
74315
  import_picocolors25.default.dim(
72684
74316
  ` update it:
72685
- uv tool install axoniq==${PINNED_SOURCE_VERSION}
72686
- or: pip install axoniq==${PINNED_SOURCE_VERSION}`
74317
+ pip install --upgrade horus-source`
72687
74318
  )
72688
74319
  );
72689
74320
  } else {
@@ -72787,12 +74418,192 @@ async function runSetup(opts) {
72787
74418
  init_cjs_shims();
72788
74419
  var import_node_readline = require("readline");
72789
74420
  var import_node_child_process5 = require("child_process");
74421
+ var import_picocolors27 = __toESM(require_picocolors(), 1);
74422
+
74423
+ // ../../packages/cli/src/lib/tty-selector.ts
74424
+ init_cjs_shims();
74425
+ var import_node_tty = require("tty");
72790
74426
  var import_picocolors26 = __toESM(require_picocolors(), 1);
74427
+ var ExitPromptError = class extends Error {
74428
+ constructor(message = "Prompt was cancelled") {
74429
+ super(message);
74430
+ this.name = "ExitPromptError";
74431
+ }
74432
+ };
74433
+ function isInteractive(input) {
74434
+ const stream = input ?? process.stdin;
74435
+ return stream.isTTY === true && typeof stream.setRawMode === "function";
74436
+ }
74437
+ async function checkboxSearch(opts) {
74438
+ const { message, choices } = opts;
74439
+ const pageSize = opts.pageSize ?? 10;
74440
+ const input = opts.input ?? process.stdin;
74441
+ const output = opts.output ?? process.stdout;
74442
+ if (!isInteractive(input)) {
74443
+ throw new Error("checkboxSearch requires an interactive TTY");
74444
+ }
74445
+ if (choices.length === 0) {
74446
+ return [];
74447
+ }
74448
+ return new Promise((resolve8, reject) => {
74449
+ let filter = "";
74450
+ const selected = /* @__PURE__ */ new Set();
74451
+ let cursor = 0;
74452
+ let filtered = choices;
74453
+ let visibleOffset = 0;
74454
+ let lastRenderLines = 0;
74455
+ const wasRaw = input.isRaw;
74456
+ function updateFiltered() {
74457
+ const f = filter.toLowerCase();
74458
+ filtered = f ? choices.filter((c) => c.toLowerCase().includes(f)) : choices;
74459
+ cursor = Math.min(cursor, Math.max(0, filtered.length - 1));
74460
+ updateVisibleOffset();
74461
+ }
74462
+ function updateVisibleOffset() {
74463
+ if (cursor < visibleOffset) {
74464
+ visibleOffset = cursor;
74465
+ } else if (cursor >= visibleOffset + pageSize) {
74466
+ visibleOffset = cursor - pageSize + 1;
74467
+ }
74468
+ const maxOffset = Math.max(0, filtered.length - pageSize);
74469
+ visibleOffset = Math.max(0, Math.min(visibleOffset, maxOffset));
74470
+ }
74471
+ function clear() {
74472
+ output.write("\x1B[?25l");
74473
+ if (lastRenderLines > 0) {
74474
+ output.write(`\x1B[${lastRenderLines}A`);
74475
+ output.write("\x1B[0J");
74476
+ }
74477
+ }
74478
+ function render() {
74479
+ clear();
74480
+ const lines = [];
74481
+ lines.push(
74482
+ `? ${import_picocolors26.default.bold(message)} ${import_picocolors26.default.dim("(\u2191\u2193 navigate \u2022 space toggle \u2022 enter confirm \u2022 type filter)")}`
74483
+ );
74484
+ const visible = filtered.slice(visibleOffset, visibleOffset + pageSize);
74485
+ for (let i = 0; i < visible.length; i++) {
74486
+ const idx = visibleOffset + i;
74487
+ const choice = visible[i];
74488
+ const isCursor = idx === cursor;
74489
+ const isSelected = selected.has(choice);
74490
+ const marker = isSelected ? import_picocolors26.default.green("\u25CF") : " ";
74491
+ const prefix = `[${marker}]`;
74492
+ const label = isCursor ? import_picocolors26.default.cyan(choice) : choice;
74493
+ const pointer = isCursor ? import_picocolors26.default.cyan("\u276F") : " ";
74494
+ lines.push(` ${pointer} ${prefix} ${label}`);
74495
+ }
74496
+ if (filtered.length === 0) {
74497
+ lines.push(import_picocolors26.default.dim(" No matches"));
74498
+ }
74499
+ const statusParts = [
74500
+ `${selected.size} selected`,
74501
+ `${filtered.length}/${choices.length} matches`
74502
+ ];
74503
+ if (filter) statusParts.push(`filter: ${filter}`);
74504
+ lines.push(import_picocolors26.default.dim(` ${statusParts.join(" \xB7 ")}`));
74505
+ output.write(lines.join("\n"));
74506
+ lastRenderLines = lines.length;
74507
+ }
74508
+ function finish(values2) {
74509
+ cleanup();
74510
+ const summary = values2.length > 0 ? values2.join(", ") : import_picocolors26.default.dim("none");
74511
+ output.write(`
74512
+ ? ${import_picocolors26.default.bold(message)} ${summary}
74513
+ `);
74514
+ resolve8(values2);
74515
+ }
74516
+ function cancel() {
74517
+ cleanup();
74518
+ reject(new ExitPromptError());
74519
+ }
74520
+ function cleanup() {
74521
+ input.setRawMode(wasRaw);
74522
+ input.pause();
74523
+ input.removeListener("data", onData);
74524
+ output.write("\x1B[?25h");
74525
+ }
74526
+ function toggleCurrent() {
74527
+ const choice = filtered[cursor];
74528
+ if (choice === void 0) return;
74529
+ if (selected.has(choice)) {
74530
+ selected.delete(choice);
74531
+ } else {
74532
+ selected.add(choice);
74533
+ }
74534
+ }
74535
+ function onData(chunk) {
74536
+ const s = chunk.toString();
74537
+ if (s === "") {
74538
+ cancel();
74539
+ return;
74540
+ }
74541
+ if (s === "\x1B") {
74542
+ if (filter) {
74543
+ filter = "";
74544
+ cursor = 0;
74545
+ updateFiltered();
74546
+ } else {
74547
+ cancel();
74548
+ }
74549
+ render();
74550
+ return;
74551
+ }
74552
+ if (s === "\r" || s === "\n") {
74553
+ finish(Array.from(selected));
74554
+ return;
74555
+ }
74556
+ if (s === " ") {
74557
+ toggleCurrent();
74558
+ render();
74559
+ return;
74560
+ }
74561
+ if (s === "\x7F" || s === "\b") {
74562
+ if (filter.length > 0) {
74563
+ filter = filter.slice(0, -1);
74564
+ cursor = 0;
74565
+ updateFiltered();
74566
+ }
74567
+ render();
74568
+ return;
74569
+ }
74570
+ if (s === "\x1B[A") {
74571
+ if (cursor > 0) {
74572
+ cursor -= 1;
74573
+ updateVisibleOffset();
74574
+ }
74575
+ render();
74576
+ return;
74577
+ }
74578
+ if (s === "\x1B[B") {
74579
+ if (cursor < filtered.length - 1) {
74580
+ cursor += 1;
74581
+ updateVisibleOffset();
74582
+ }
74583
+ render();
74584
+ return;
74585
+ }
74586
+ if (s.length === 1 && s.charCodeAt(0) >= 32 && s.charCodeAt(0) <= 126) {
74587
+ filter += s;
74588
+ cursor = 0;
74589
+ updateFiltered();
74590
+ render();
74591
+ }
74592
+ }
74593
+ input.setRawMode(true);
74594
+ input.resume();
74595
+ input.on("data", onData);
74596
+ updateFiltered();
74597
+ render();
74598
+ });
74599
+ }
74600
+
74601
+ // ../../packages/cli/src/commands/connect.ts
72791
74602
  var SUPPORTED = ["elasticsearch", "mongodb", "grafana", "redis"];
72792
74603
  async function runConnect(type, opts) {
72793
74604
  if (!SUPPORTED.includes(type)) {
72794
74605
  console.error(
72795
- import_picocolors26.default.red(`Unknown connector type: ${type}`) + import_picocolors26.default.dim(`
74606
+ import_picocolors27.default.red(`Unknown connector type: ${type}`) + import_picocolors27.default.dim(`
72796
74607
  supported: ${SUPPORTED.join(", ")}`)
72797
74608
  );
72798
74609
  return 1;
@@ -72823,18 +74634,20 @@ async function runConnect(type, opts) {
72823
74634
  if (!probeResult.ok) {
72824
74635
  console.error(
72825
74636
  `
72826
- ${import_picocolors26.default.red(`\u2717 Could not reach ${connectorType}:`)} ${probeResult.detail}` + import_picocolors26.default.dim("\n Fix the connection and retry, or pass --no-test to skip.")
74637
+ ${import_picocolors27.default.red(`\u2717 Could not reach ${connectorType}:`)} ${probeResult.detail}` + import_picocolors27.default.dim("\n Fix the connection and retry, or pass --no-test to skip.")
72827
74638
  );
72828
74639
  return 1;
72829
74640
  }
72830
- console.log(`
72831
- ${import_picocolors26.default.green("\u2713")} ${connectorType} reachable ${import_picocolors26.default.dim(`(${probeResult.detail})`)}`);
74641
+ console.log(
74642
+ `
74643
+ ${import_picocolors27.default.green("\u2713")} ${connectorType} reachable ${import_picocolors27.default.dim(`(${probeResult.detail})`)}`
74644
+ );
72832
74645
  }
72833
74646
  const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0;
72834
74647
  if (hasLiteralCredentials) {
72835
74648
  if (isGitTracked(configPath, root)) {
72836
74649
  console.error(
72837
- import_picocolors26.default.red(".horus/config.json is already tracked by Git.") + "\nStoring credentials here would expose them in the repository.\n" + import_picocolors26.default.dim(
74650
+ import_picocolors27.default.red(".horus/config.json is already tracked by Git.") + "\nStoring credentials here would expose them in the repository.\n" + import_picocolors27.default.dim(
72838
74651
  " Option A \u2014 remove from Git index then re-run:\n git rm --cached .horus/config.json\n horus connect " + type + " ...\n\n Option B \u2014 keep credentials in the environment and reference them:\n export ES_URL=https://...\n export ES_USERNAME=...\n export ES_PASSWORD=...\n Then edit .horus/config.json manually to use urlEnv/usernameEnv/passwordEnv."
72839
74652
  )
72840
74653
  );
@@ -72843,29 +74656,52 @@ ${import_picocolors26.default.green("\u2713")} ${connectorType} reachable ${impo
72843
74656
  ensureCredentialGitignore(root);
72844
74657
  }
72845
74658
  patchLocalConnector(configPath, connectorType, patch, filled.env);
72846
- console.log(`${import_picocolors26.default.green("\u2713")} ${import_picocolors26.default.bold(connectorType)} connector saved \u2192 ${import_picocolors26.default.dim(configPath)}`);
74659
+ console.log(
74660
+ `${import_picocolors27.default.green("\u2713")} ${import_picocolors27.default.bold(connectorType)} connector saved \u2192 ${import_picocolors27.default.dim(configPath)}`
74661
+ );
72847
74662
  printSummary(connectorType, filled);
72848
- console.log(import_picocolors26.default.dim(`
74663
+ console.log(import_picocolors27.default.dim(`
72849
74664
  run: horus investigate "<hint>"`));
72850
74665
  return 0;
72851
74666
  } catch (err) {
72852
- console.error(import_picocolors26.default.red(err.message));
74667
+ if (err instanceof ExitPromptError) {
74668
+ console.error(import_picocolors27.default.red("Cancelled."));
74669
+ return 1;
74670
+ }
74671
+ console.error(import_picocolors27.default.red(err.message));
72853
74672
  return 1;
72854
74673
  }
72855
74674
  }
72856
74675
  async function fillInteractive(type, opts) {
72857
74676
  const needsInteraction = missingRequired(type, opts);
72858
74677
  if (!needsInteraction) return opts;
72859
- console.log(`
72860
- ${import_picocolors26.default.bold(`Connect ${type}`)} ${import_picocolors26.default.dim("(press Enter to skip optional fields)")}
72861
- `);
74678
+ console.log(
74679
+ `
74680
+ ${import_picocolors27.default.bold(`Connect ${type}`)} ${import_picocolors27.default.dim("(press Enter to skip optional fields)")}
74681
+ `
74682
+ );
72862
74683
  const filled = { ...opts };
72863
74684
  switch (type) {
72864
74685
  case "elasticsearch":
72865
74686
  filled.url = filled.url ?? await ask("URL", "https://elastic.example.com");
72866
74687
  filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
72867
74688
  filled.password = filled.password ?? (await askPassword("Password") || void 0);
72868
- filled.indexPattern = filled.indexPattern ?? await ask("Index pattern", "logs-*");
74689
+ if (filled.indexPattern === void 0 && filled.indexPatterns === void 0) {
74690
+ const discovered = await discoverEsIndices(
74691
+ filled.url,
74692
+ filled.username,
74693
+ filled.password
74694
+ );
74695
+ if (discovered.length > 0) {
74696
+ const selected = await askIndexSelection(discovered);
74697
+ if (selected.length > 0) {
74698
+ filled.indexPatterns = selected;
74699
+ }
74700
+ }
74701
+ if (filled.indexPatterns === void 0) {
74702
+ filled.indexPattern = await ask("Index pattern", "logs-*");
74703
+ }
74704
+ }
72869
74705
  filled.service = filled.service ?? (await ask("Service name", "") || void 0);
72870
74706
  break;
72871
74707
  case "mongodb":
@@ -72877,18 +74713,45 @@ ${import_picocolors26.default.bold(`Connect ${type}`)} ${import_picocolors26.def
72877
74713
  filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
72878
74714
  filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
72879
74715
  filled.password = filled.password ?? (await askPassword("Password") || void 0);
72880
- filled.dashboard = filled.dashboard ?? (await ask("Default dashboard uid", "") || void 0);
74716
+ if (filled.dashboard === void 0 && filled.dashboardUids === void 0) {
74717
+ const discovered = await discoverGrafanaDashboards(
74718
+ filled.url,
74719
+ filled.username,
74720
+ filled.password
74721
+ );
74722
+ if (discovered.length > 0) {
74723
+ const selected = await askDashboardSelection(discovered);
74724
+ if (selected.length > 0) {
74725
+ filled.dashboardUids = selected.map((d) => d.uid);
74726
+ }
74727
+ }
74728
+ if (filled.dashboardUids === void 0) {
74729
+ filled.dashboard = await ask("Default dashboard uid", "", false) || void 0;
74730
+ }
74731
+ }
72881
74732
  break;
72882
- case "redis":
74733
+ case "redis": {
74734
+ console.log(
74735
+ import_picocolors27.default.dim(
74736
+ " Tip: embed credentials directly in the URL \u2014 redis://:password@host:6379\n or enter the URL and password separately below."
74737
+ )
74738
+ );
72883
74739
  filled.url = filled.url ?? await ask("URL", "redis://localhost:6379");
74740
+ if (!redisUrlHasPassword(filled.url)) {
74741
+ const pw = await askPassword("Password") || void 0;
74742
+ if (pw !== void 0 && filled.url !== void 0) {
74743
+ filled.url = injectRedisPassword(filled.url, pw);
74744
+ }
74745
+ }
72884
74746
  break;
74747
+ }
72885
74748
  }
72886
74749
  return filled;
72887
74750
  }
72888
74751
  function missingRequired(type, opts) {
72889
74752
  switch (type) {
72890
74753
  case "elasticsearch":
72891
- return !opts.url || !opts.indexPattern;
74754
+ return !opts.url || !opts.indexPattern && !opts.indexPatterns?.length;
72892
74755
  case "mongodb":
72893
74756
  return !opts.url || !opts.database;
72894
74757
  case "grafana":
@@ -72899,10 +74762,14 @@ function missingRequired(type, opts) {
72899
74762
  }
72900
74763
  function ask(label, placeholder = "", required = true) {
72901
74764
  return new Promise((resolve8) => {
72902
- const hint = placeholder ? import_picocolors26.default.dim(` (${placeholder})`) : "";
72903
- const suffix = required ? "" : import_picocolors26.default.dim(" [optional]");
74765
+ const hint = placeholder ? import_picocolors27.default.dim(` (${placeholder})`) : "";
74766
+ const suffix = required ? "" : import_picocolors27.default.dim(" [optional]");
72904
74767
  process.stdout.write(` ${label}${suffix}${hint}: `);
72905
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout, terminal: false });
74768
+ const rl = (0, import_node_readline.createInterface)({
74769
+ input: process.stdin,
74770
+ output: process.stdout,
74771
+ terminal: false
74772
+ });
72906
74773
  rl.once("line", (line2) => {
72907
74774
  rl.close();
72908
74775
  resolve8(line2.trim() || (required ? placeholder : ""));
@@ -72913,7 +74780,7 @@ function askPassword(label) {
72913
74780
  return new Promise((resolve8) => {
72914
74781
  const stdin = process.stdin;
72915
74782
  if (typeof stdin.setRawMode === "function") {
72916
- process.stdout.write(` ${label}${import_picocolors26.default.dim(" [optional]")}: `);
74783
+ process.stdout.write(` ${label}${import_picocolors27.default.dim(" [optional]")}: `);
72917
74784
  stdin.setRawMode(true);
72918
74785
  stdin.resume();
72919
74786
  let value = "";
@@ -72944,8 +74811,15 @@ function askPassword(label) {
72944
74811
  });
72945
74812
  }
72946
74813
  function buildEsPatch(opts) {
72947
- if (!opts.indexPattern) throw new Error("Index pattern is required for elasticsearch");
72948
- const patch = { indexPattern: opts.indexPattern };
74814
+ if (!opts.indexPattern && !opts.indexPatterns?.length) {
74815
+ throw new Error("Index pattern is required for elasticsearch");
74816
+ }
74817
+ const patch = {};
74818
+ if (opts.indexPatterns && opts.indexPatterns.length > 0) {
74819
+ patch["indexPatterns"] = opts.indexPatterns;
74820
+ } else {
74821
+ patch["indexPattern"] = opts.indexPattern;
74822
+ }
72949
74823
  if (opts.url) patch["url"] = opts.url;
72950
74824
  if (opts.username) patch["username"] = opts.username;
72951
74825
  if (opts.password) patch["password"] = opts.password;
@@ -72966,7 +74840,11 @@ function buildGrafanaPatch(opts) {
72966
74840
  if (opts.url) patch["url"] = opts.url;
72967
74841
  if (opts.username) patch["username"] = opts.username;
72968
74842
  if (opts.password) patch["password"] = opts.password;
72969
- if (opts.dashboard) patch["dashboard"] = opts.dashboard;
74843
+ if (opts.dashboardUids && opts.dashboardUids.length > 0) {
74844
+ patch["dashboards"] = opts.dashboardUids;
74845
+ } else if (opts.dashboard) {
74846
+ patch["dashboard"] = opts.dashboard;
74847
+ }
72970
74848
  return patch;
72971
74849
  }
72972
74850
  function buildRedisPatch(opts) {
@@ -72985,7 +74863,10 @@ async function probe(type, opts) {
72985
74863
  password: opts.password
72986
74864
  });
72987
74865
  const controller = new AbortController();
72988
- const timer2 = setTimeout(() => controller.abort(new Error("timed out after 8s")), 8e3);
74866
+ const timer2 = setTimeout(
74867
+ () => controller.abort(new Error("timed out after 8s")),
74868
+ 8e3
74869
+ );
72989
74870
  try {
72990
74871
  return await client.health(controller.signal);
72991
74872
  } catch (err) {
@@ -73012,7 +74893,9 @@ async function probe(type, opts) {
73012
74893
  if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
73013
74894
  const headers = { "Content-Type": "application/json" };
73014
74895
  if (opts.username && opts.password) {
73015
- const encoded = Buffer.from(`${opts.username}:${opts.password}`).toString("base64");
74896
+ const encoded = Buffer.from(`${opts.username}:${opts.password}`).toString(
74897
+ "base64"
74898
+ );
73016
74899
  headers["Authorization"] = `Basic ${encoded}`;
73017
74900
  }
73018
74901
  const res = await fetch(`${opts.url.replace(/\/$/, "")}/api/health`, {
@@ -73056,15 +74939,24 @@ function tcpProbe(host, port, timeoutMs = 3e3) {
73056
74939
  }
73057
74940
  function printSummary(type, opts) {
73058
74941
  const lines = [];
73059
- if (opts.url) lines.push(` url: ${redactUrl(opts.url)}`);
73060
- if (opts.username) lines.push(` username: ${opts.username}`);
73061
- if (opts.password) lines.push(` password: ${"\u2022".repeat(Math.min(opts.password.length, 8))}`);
73062
- if (opts.indexPattern) lines.push(` index-pattern: ${opts.indexPattern}`);
73063
- if (opts.service) lines.push(` service: ${opts.service}`);
73064
- if (opts.database) lines.push(` database: ${opts.database}`);
73065
- if (opts.collections) lines.push(` collections: ${opts.collections}`);
73066
- if (opts.dashboard) lines.push(` dashboard: ${opts.dashboard}`);
73067
- if (lines.length > 0) console.log(import_picocolors26.default.dim(lines.join("\n")));
74942
+ if (opts.url) lines.push(` url: ${redactUrl(opts.url)}`);
74943
+ if (opts.username) lines.push(` username: ${opts.username}`);
74944
+ if (opts.password)
74945
+ lines.push(` password: ${"\u2022".repeat(Math.min(opts.password.length, 8))}`);
74946
+ if (opts.indexPatterns && opts.indexPatterns.length > 0) {
74947
+ lines.push(` index-patterns: ${opts.indexPatterns.join(", ")}`);
74948
+ } else if (opts.indexPattern) {
74949
+ lines.push(` index-pattern: ${opts.indexPattern}`);
74950
+ }
74951
+ if (opts.service) lines.push(` service: ${opts.service}`);
74952
+ if (opts.database) lines.push(` database: ${opts.database}`);
74953
+ if (opts.collections) lines.push(` collections: ${opts.collections}`);
74954
+ if (opts.dashboardUids && opts.dashboardUids.length > 0) {
74955
+ lines.push(` dashboards: ${opts.dashboardUids.join(", ")}`);
74956
+ } else if (opts.dashboard) {
74957
+ lines.push(` dashboard: ${opts.dashboard}`);
74958
+ }
74959
+ if (lines.length > 0) console.log(import_picocolors27.default.dim(lines.join("\n")));
73068
74960
  }
73069
74961
  function isGitTracked(filePath, cwd) {
73070
74962
  try {
@@ -73077,6 +74969,119 @@ function isGitTracked(filePath, cwd) {
73077
74969
  return false;
73078
74970
  }
73079
74971
  }
74972
+ async function discoverEsIndices(url, username, password) {
74973
+ if (!url) return [];
74974
+ try {
74975
+ const client = new ElasticsearchClient({ baseUrl: url, username, password });
74976
+ const signal = AbortSignal.timeout(8e3);
74977
+ return await client.listIndices(signal);
74978
+ } catch {
74979
+ return [];
74980
+ }
74981
+ }
74982
+ async function askIndexSelection(indices) {
74983
+ if (isInteractive()) {
74984
+ try {
74985
+ return await checkboxSearch({
74986
+ message: "Select index patterns to use",
74987
+ choices: indices,
74988
+ pageSize: 12
74989
+ });
74990
+ } catch (err) {
74991
+ if (err instanceof ExitPromptError) throw err;
74992
+ }
74993
+ }
74994
+ const MAX_DISPLAY = 25;
74995
+ const shown = indices.slice(0, MAX_DISPLAY);
74996
+ console.log("\n Available Elasticsearch indexes/data streams:");
74997
+ shown.forEach((name, i) => {
74998
+ console.log(` ${import_picocolors27.default.dim(`[${i + 1}]`)} ${name}`);
74999
+ });
75000
+ if (indices.length > MAX_DISPLAY) {
75001
+ console.log(
75002
+ import_picocolors27.default.dim(
75003
+ ` \u2026 and ${indices.length - MAX_DISPLAY} more (type a pattern manually to match all)`
75004
+ )
75005
+ );
75006
+ }
75007
+ const input = (await ask(
75008
+ ` Select index patterns to use (e.g. 1,2 or Enter to type pattern manually)`,
75009
+ "",
75010
+ false
75011
+ )).trim();
75012
+ if (!input) return [];
75013
+ if (/^[\d,\s]+$/.test(input)) {
75014
+ const picks = input.split(",").map((s) => s.trim()).filter(Boolean).map((s) => parseInt(s, 10) - 1).filter((i) => i >= 0 && i < shown.length).map((i) => shown[i]);
75015
+ if (picks.length > 0) return picks;
75016
+ }
75017
+ return [input];
75018
+ }
75019
+ async function discoverGrafanaDashboards(url, username, password) {
75020
+ if (!url) return [];
75021
+ try {
75022
+ const client = new GrafanaClient({ baseUrl: url, username, password });
75023
+ const signal = AbortSignal.timeout(8e3);
75024
+ return await client.searchDashboards(void 0, signal);
75025
+ } catch {
75026
+ return [];
75027
+ }
75028
+ }
75029
+ async function askDashboardSelection(dashboards) {
75030
+ if (isInteractive()) {
75031
+ try {
75032
+ const selected = await checkboxSearch({
75033
+ message: "Select dashboards to use",
75034
+ choices: dashboards.map((d) => d.title),
75035
+ pageSize: 12
75036
+ });
75037
+ return dashboards.filter((d) => selected.includes(d.title));
75038
+ } catch (err) {
75039
+ if (err instanceof ExitPromptError) throw err;
75040
+ }
75041
+ }
75042
+ const MAX_DISPLAY = 25;
75043
+ const shown = dashboards.slice(0, MAX_DISPLAY);
75044
+ console.log("\n Available Grafana dashboards:");
75045
+ shown.forEach((d, i) => {
75046
+ const folder = d.folderTitle ? import_picocolors27.default.dim(` (${d.folderTitle})`) : "";
75047
+ console.log(` ${import_picocolors27.default.dim(`[${i + 1}]`)} ${d.title}${folder}`);
75048
+ });
75049
+ if (dashboards.length > MAX_DISPLAY) {
75050
+ console.log(import_picocolors27.default.dim(` \u2026 and ${dashboards.length - MAX_DISPLAY} more`));
75051
+ }
75052
+ const input = (await ask(
75053
+ ` Select dashboards to use (e.g. 1,2 or Enter to type uid manually)`,
75054
+ "",
75055
+ false
75056
+ )).trim();
75057
+ if (!input) return [];
75058
+ if (/^[\d,\s]+$/.test(input)) {
75059
+ const picks = input.split(",").map((s) => s.trim()).filter(Boolean).map((s) => parseInt(s, 10) - 1).filter((i) => i >= 0 && i < shown.length).map((i) => shown[i]);
75060
+ return picks;
75061
+ }
75062
+ return [];
75063
+ }
75064
+ function redisUrlHasPassword(raw) {
75065
+ if (!raw) return false;
75066
+ try {
75067
+ return new URL(raw).password !== "";
75068
+ } catch {
75069
+ return false;
75070
+ }
75071
+ }
75072
+ function injectRedisPassword(raw, password) {
75073
+ try {
75074
+ const u = new URL(raw);
75075
+ u.password = encodeURIComponent(password);
75076
+ return u.toString();
75077
+ } catch {
75078
+ const match = /^(rediss?:\/\/)(.*)$/.exec(raw);
75079
+ if (match?.[1] && match?.[2]) {
75080
+ return `${match[1]}:${encodeURIComponent(password)}@${match[2]}`;
75081
+ }
75082
+ return raw;
75083
+ }
75084
+ }
73080
75085
  function redactUrl(raw) {
73081
75086
  try {
73082
75087
  const u = new URL(raw);
@@ -73096,11 +75101,14 @@ var import_node_child_process6 = require("child_process");
73096
75101
  var import_node_fs7 = require("fs");
73097
75102
  var import_node_util4 = require("util");
73098
75103
  var import_node_path9 = require("path");
73099
- var import_picocolors27 = __toESM(require_picocolors(), 1);
75104
+ var import_picocolors28 = __toESM(require_picocolors(), 1);
73100
75105
  var execFileAsync = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
73101
75106
  var unlinkAsync = (0, import_node_util4.promisify)(import_node_fs7.unlink);
73102
75107
  var SPAWNED_HOST_FILE2 = "spawned-host.json";
73103
75108
  var START_TIME_TOLERANCE_S = 60;
75109
+ var STOP_WAIT_MS = 5e3;
75110
+ var STOP_POLL_MS = 200;
75111
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
73104
75112
  async function runStop(opts) {
73105
75113
  try {
73106
75114
  if (opts.all) {
@@ -73110,30 +75118,30 @@ async function runStop(opts) {
73110
75118
  const root = findRepoRoot(cwd) ?? cwd;
73111
75119
  const hostUrl = readSourceHostUrl(root);
73112
75120
  if (!hostUrl) {
73113
- console.log(import_picocolors27.default.dim("No source-intelligence host found for this repo (.horus/source/host.json absent)."));
75121
+ console.log(import_picocolors28.default.dim("No source-intelligence host found for this repo (.horus/source/host.json absent)."));
73114
75122
  return 0;
73115
75123
  }
73116
75124
  return await stopHost(root, hostUrl);
73117
75125
  } catch (err) {
73118
- console.error(import_picocolors27.default.red(err.message));
75126
+ console.error(import_picocolors28.default.red(err.message));
73119
75127
  return 1;
73120
75128
  }
73121
75129
  }
73122
75130
  async function stopHost(root, hostUrl) {
73123
75131
  const alive = await isHostHealthy(hostUrl);
73124
75132
  if (!alive) {
73125
- console.log(import_picocolors27.default.dim(`Host ${hostUrl} is already stopped.`));
75133
+ console.log(import_picocolors28.default.dim(`Host ${hostUrl} is already stopped.`));
73126
75134
  return 0;
73127
75135
  }
73128
75136
  const port = extractPort(hostUrl);
73129
75137
  if (port === null) {
73130
- console.error(import_picocolors27.default.red(`Cannot determine port from host URL: ${hostUrl}`));
75138
+ console.error(import_picocolors28.default.red(`Cannot determine port from host URL: ${hostUrl}`));
73131
75139
  return 1;
73132
75140
  }
73133
75141
  const spawned = readSpawnedHost(root);
73134
75142
  if (spawned === null) {
73135
75143
  console.error(
73136
- import_picocolors27.default.red(
75144
+ import_picocolors28.default.red(
73137
75145
  `No ownership record found (.horus/${SPAWNED_HOST_FILE2} absent). Horus will not stop a host it did not spawn.`
73138
75146
  )
73139
75147
  );
@@ -73141,12 +75149,12 @@ async function stopHost(root, hostUrl) {
73141
75149
  }
73142
75150
  const recordError = validateSpawnedRecord(spawned);
73143
75151
  if (recordError !== null) {
73144
- console.error(import_picocolors27.default.red(`Ownership record is malformed: ${recordError}. Aborting for safety.`));
75152
+ console.error(import_picocolors28.default.red(`Ownership record is malformed: ${recordError}. Aborting for safety.`));
73145
75153
  return 1;
73146
75154
  }
73147
75155
  if (spawned.port !== port) {
73148
75156
  console.error(
73149
- import_picocolors27.default.red(
75157
+ import_picocolors28.default.red(
73150
75158
  `Ownership record port (${spawned.port}) does not match host URL port (${port}). Record may be stale.`
73151
75159
  )
73152
75160
  );
@@ -73154,7 +75162,7 @@ async function stopHost(root, hostUrl) {
73154
75162
  }
73155
75163
  if (spawned.root !== root) {
73156
75164
  console.error(
73157
- import_picocolors27.default.red(
75165
+ import_picocolors28.default.red(
73158
75166
  `Ownership record root (${spawned.root}) does not match resolved root (${root}). Record may be stale.`
73159
75167
  )
73160
75168
  );
@@ -73162,17 +75170,21 @@ async function stopHost(root, hostUrl) {
73162
75170
  }
73163
75171
  const info = await getProcessInfo(spawned.pid);
73164
75172
  if (info === null) {
73165
- console.error(import_picocolors27.default.red(`Process pid ${spawned.pid} is no longer running.`));
73166
- return 1;
75173
+ console.log(import_picocolors28.default.dim(`Process pid ${spawned.pid} is no longer running \u2014 already stopped.`));
75174
+ try {
75175
+ await unlinkAsync((0, import_node_path9.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
75176
+ } catch {
75177
+ }
75178
+ return 0;
73167
75179
  }
73168
75180
  const portStr = String(port);
73169
- const axonHostPortRe = new RegExp(
73170
- `(?:^|\\s)(?:\\S*/)?axon\\s+host\\s+--port(?:=|\\s+)${portStr}(?=\\s|$)`
75181
+ const hostPortRe = new RegExp(
75182
+ `(?:^|\\s)(?:\\S*/)?horus-source\\s+host\\s+--port(?:=|\\s+)${portStr}(?=\\s|$)`
73171
75183
  );
73172
- if (!axonHostPortRe.test(info.args)) {
75184
+ if (!hostPortRe.test(info.args)) {
73173
75185
  console.error(
73174
- import_picocolors27.default.red(
73175
- `Pid ${spawned.pid} args do not match "axon host --port ${portStr}". Got: "${info.args.slice(0, 120)}". Aborting for safety.`
75186
+ import_picocolors28.default.red(
75187
+ `Pid ${spawned.pid} args do not match "horus-source host --port ${portStr}". Got: "${info.args.slice(0, 120)}". Aborting for safety.`
73176
75188
  )
73177
75189
  );
73178
75190
  return 1;
@@ -73180,29 +75192,50 @@ async function stopHost(root, hostUrl) {
73180
75192
  const startTs = new Date(spawned.startedAt).getTime();
73181
75193
  const recordedAgeS = Math.round((Date.now() - startTs) / 1e3);
73182
75194
  if (!Number.isFinite(info.etimeSeconds)) {
73183
- console.error(import_picocolors27.default.red(`Could not read elapsed time for pid ${spawned.pid}. Aborting for safety.`));
75195
+ console.error(import_picocolors28.default.red(`Could not read elapsed time for pid ${spawned.pid}. Aborting for safety.`));
73184
75196
  return 1;
73185
75197
  }
73186
75198
  if (Math.abs(info.etimeSeconds - recordedAgeS) > START_TIME_TOLERANCE_S) {
73187
75199
  console.error(
73188
- import_picocolors27.default.red(
75200
+ import_picocolors28.default.red(
73189
75201
  `Pid ${spawned.pid} age mismatch: record says ~${recordedAgeS}s, process reports ${info.etimeSeconds}s elapsed. Possible PID reuse \u2014 aborting for safety.`
73190
75202
  )
73191
75203
  );
73192
75204
  return 1;
73193
75205
  }
75206
+ let signaled = false;
73194
75207
  try {
73195
75208
  process.kill(spawned.pid, "SIGTERM");
73196
- console.log(
73197
- `${import_picocolors27.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors27.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
73198
- );
75209
+ signaled = true;
73199
75210
  } catch (err) {
73200
75211
  if (err.code === "ESRCH") {
73201
- console.log(import_picocolors27.default.dim(`Host already gone (pid ${spawned.pid}).`));
75212
+ console.log(import_picocolors28.default.dim(`Process pid ${spawned.pid} exited before signal \u2014 already stopped.`));
73202
75213
  } else {
73203
- console.error(import_picocolors27.default.red(`Failed to signal pid ${spawned.pid}: ${err.message}`));
75214
+ console.error(import_picocolors28.default.red(`Failed to signal pid ${spawned.pid}: ${err.message}`));
75215
+ return 1;
75216
+ }
75217
+ }
75218
+ if (signaled) {
75219
+ const deadline = Date.now() + STOP_WAIT_MS;
75220
+ let exited = false;
75221
+ while (Date.now() < deadline) {
75222
+ await sleep(STOP_POLL_MS);
75223
+ if (await getProcessInfo(spawned.pid) === null) {
75224
+ exited = true;
75225
+ break;
75226
+ }
75227
+ }
75228
+ if (!exited) {
75229
+ console.error(
75230
+ import_picocolors28.default.red(
75231
+ `Host pid ${spawned.pid} did not exit within ${STOP_WAIT_MS / 1e3}s after SIGTERM.`
75232
+ )
75233
+ );
73204
75234
  return 1;
73205
75235
  }
75236
+ console.log(
75237
+ `${import_picocolors28.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors28.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
75238
+ );
73206
75239
  }
73207
75240
  try {
73208
75241
  await unlinkAsync((0, import_node_path9.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
@@ -73214,7 +75247,7 @@ async function stopAll() {
73214
75247
  const registry = readRegistry();
73215
75248
  const projects2 = Object.entries(registry.projects);
73216
75249
  if (projects2.length === 0) {
73217
- console.log(import_picocolors27.default.dim("No registered projects."));
75250
+ console.log(import_picocolors28.default.dim("No registered projects."));
73218
75251
  return 0;
73219
75252
  }
73220
75253
  let stopped = 0;
@@ -73224,17 +75257,17 @@ async function stopAll() {
73224
75257
  if (!hostUrl) continue;
73225
75258
  const alive = await isHostHealthy(hostUrl);
73226
75259
  if (!alive) continue;
73227
- console.log(` Stopping ${import_picocolors27.default.bold(name)} ${import_picocolors27.default.dim(`(${hostUrl})`)}`);
75260
+ console.log(` Stopping ${import_picocolors28.default.bold(name)} ${import_picocolors28.default.dim(`(${hostUrl})`)}`);
73228
75261
  const code = await stopHost(entry2.root, hostUrl);
73229
75262
  if (code === 0) stopped++;
73230
75263
  else failed++;
73231
75264
  }
73232
75265
  if (stopped === 0 && failed === 0) {
73233
- console.log(import_picocolors27.default.dim("No running source-intelligence hosts found."));
75266
+ console.log(import_picocolors28.default.dim("No running source-intelligence hosts found."));
73234
75267
  } else {
73235
75268
  console.log(
73236
75269
  `
73237
- Stopped ${stopped} host(s)${failed > 0 ? import_picocolors27.default.red(`, ${failed} failed`) : ""}.`
75270
+ Stopped ${stopped} host(s)${failed > 0 ? import_picocolors28.default.red(`, ${failed} failed`) : ""}.`
73238
75271
  );
73239
75272
  }
73240
75273
  return failed > 0 ? 1 : 0;
@@ -73286,12 +75319,12 @@ function extractPort(hostUrl) {
73286
75319
 
73287
75320
  // ../../packages/cli/src/commands/hosts.ts
73288
75321
  init_cjs_shims();
73289
- var import_picocolors28 = __toESM(require_picocolors(), 1);
75322
+ var import_picocolors29 = __toESM(require_picocolors(), 1);
73290
75323
  async function runHosts() {
73291
75324
  const registry = readRegistry();
73292
75325
  const projects2 = Object.entries(registry.projects);
73293
75326
  if (projects2.length === 0) {
73294
- console.log(import_picocolors28.default.dim("No registered projects. Run `horus index` in a repo first."));
75327
+ console.log(import_picocolors29.default.dim("No registered projects. Run `horus index` in a repo first."));
73295
75328
  return 0;
73296
75329
  }
73297
75330
  const rows = [];
@@ -73308,29 +75341,29 @@ async function runHosts() {
73308
75341
  });
73309
75342
  const anyHost = rows.some((r) => r.hostUrl !== null);
73310
75343
  if (!anyHost) {
73311
- console.log(import_picocolors28.default.dim("No source-intelligence hosts found. Run `horus index` to start one."));
75344
+ console.log(import_picocolors29.default.dim("No source-intelligence hosts found. Run `horus index` to start one."));
73312
75345
  return 0;
73313
75346
  }
73314
75347
  console.log("");
73315
75348
  for (const row of rows) {
73316
75349
  if (row.hostUrl === null) continue;
73317
- const status = row.healthy ? import_picocolors28.default.green("\u25CF running") : import_picocolors28.default.red("\u25CF stopped");
75350
+ const status = row.healthy ? import_picocolors29.default.green("\u25CF running") : import_picocolors29.default.red("\u25CF stopped");
73318
75351
  const port = extractPort2(row.hostUrl) ?? "?";
73319
75352
  console.log(
73320
- ` ${status} ${import_picocolors28.default.bold(row.name.padEnd(24))} port ${String(port).padEnd(6)} ${import_picocolors28.default.dim(row.root)}`
75353
+ ` ${status} ${import_picocolors29.default.bold(row.name.padEnd(24))} port ${String(port).padEnd(6)} ${import_picocolors29.default.dim(row.root)}`
73321
75354
  );
73322
75355
  }
73323
75356
  console.log("");
73324
75357
  const noHost = rows.filter((r) => r.hostUrl === null);
73325
75358
  if (noHost.length > 0) {
73326
75359
  for (const row of noHost) {
73327
- console.log(` ${import_picocolors28.default.dim("\u25CB no host")} ${import_picocolors28.default.dim(row.name)}`);
75360
+ console.log(` ${import_picocolors29.default.dim("\u25CB no host")} ${import_picocolors29.default.dim(row.name)}`);
73328
75361
  }
73329
75362
  console.log("");
73330
75363
  }
73331
75364
  const running = rows.filter((r) => r.healthy).length;
73332
75365
  console.log(
73333
- import_picocolors28.default.dim(
75366
+ import_picocolors29.default.dim(
73334
75367
  `${running} running \xB7 horus stop to reap \xB7 horus stop --all to stop everything`
73335
75368
  )
73336
75369
  );
@@ -73347,12 +75380,12 @@ function extractPort2(hostUrl) {
73347
75380
 
73348
75381
  // ../../packages/cli/src/commands/doctor.ts
73349
75382
  init_cjs_shims();
73350
- var import_picocolors29 = __toESM(require_picocolors(), 1);
75383
+ var import_picocolors30 = __toESM(require_picocolors(), 1);
73351
75384
  var DEFAULT_DB_URL3 = "postgresql://horus:horus@localhost:5433/horus";
73352
75385
  function mark2(status) {
73353
- if (status === "pass") return import_picocolors29.default.green("\u2713");
73354
- if (status === "warn") return import_picocolors29.default.yellow("~");
73355
- return import_picocolors29.default.red("\u2717");
75386
+ if (status === "pass") return import_picocolors30.default.green("\u2713");
75387
+ if (status === "warn") return import_picocolors30.default.yellow("~");
75388
+ return import_picocolors30.default.red("\u2717");
73356
75389
  }
73357
75390
  async function runDoctor(opts) {
73358
75391
  const cwd = opts?.cwd ?? process.cwd();
@@ -73579,11 +75612,11 @@ async function runDoctor(opts) {
73579
75612
  write(JSON.stringify(output, null, 2));
73580
75613
  return hasFailure ? 1 : 0;
73581
75614
  }
73582
- write(import_picocolors29.default.bold("\nHorus readiness check\n"));
75615
+ write(import_picocolors30.default.bold("\nHorus readiness check\n"));
73583
75616
  for (const check of checks) {
73584
- write(` ${mark2(check.status)} ${import_picocolors29.default.bold(check.label.padEnd(26))} ${import_picocolors29.default.dim(check.detail)}`);
75617
+ write(` ${mark2(check.status)} ${import_picocolors30.default.bold(check.label.padEnd(26))} ${import_picocolors30.default.dim(check.detail)}`);
73585
75618
  if (check.next) {
73586
- write(` ${import_picocolors29.default.dim("\u2192 " + check.next)}`);
75619
+ write(` ${import_picocolors30.default.dim("\u2192 " + check.next)}`);
73587
75620
  }
73588
75621
  }
73589
75622
  write("");
@@ -73592,29 +75625,34 @@ async function runDoctor(opts) {
73592
75625
 
73593
75626
  // ../../packages/cli/src/commands/providers-doctor.ts
73594
75627
  init_cjs_shims();
73595
- var import_picocolors30 = __toESM(require_picocolors(), 1);
75628
+ var import_node_child_process7 = require("child_process");
75629
+ var import_picocolors31 = __toESM(require_picocolors(), 1);
73596
75630
  function statusMark(status) {
73597
- if (status === "ready") return import_picocolors30.default.green("\u2713");
73598
- if (status === "installed") return import_picocolors30.default.yellow("~");
73599
- return import_picocolors30.default.red("\u2717");
75631
+ if (status === "ready") return import_picocolors31.default.green("\u2713");
75632
+ if (status === "installed") return import_picocolors31.default.yellow("~");
75633
+ return import_picocolors31.default.red("\u2717");
73600
75634
  }
73601
75635
  function statusLabel(status) {
73602
- if (status === "ready") return import_picocolors30.default.green("ready");
73603
- if (status === "installed") return import_picocolors30.default.yellow("installed (not configured)");
73604
- return import_picocolors30.default.dim("not found on PATH");
73605
- }
73606
- function buildProviderResults(registry) {
73607
- return registry.providers.map((p) => ({
73608
- id: p.id,
73609
- status: "unavailable",
73610
- detail: `install the ${p.displayName} binary to use this provider`
73611
- }));
75636
+ if (status === "ready") return import_picocolors31.default.green("ready");
75637
+ if (status === "installed") return import_picocolors31.default.yellow("installed (not configured)");
75638
+ return import_picocolors31.default.dim("not found on PATH");
75639
+ }
75640
+ function detectBinary(id) {
75641
+ const result = (0, import_node_child_process7.spawnSync)(id, ["--version"], { stdio: "pipe", timeout: 2e3 });
75642
+ if (result.error) {
75643
+ return { id, status: "unavailable", detail: `${id}: command not found` };
75644
+ }
75645
+ return { id, status: "installed", detail: `${id}: found on PATH` };
75646
+ }
75647
+ function buildProviderResults(registry, _detect) {
75648
+ const detect = _detect ?? detectBinary;
75649
+ return registry.providers.map((p) => detect(p.id));
73612
75650
  }
73613
75651
  async function runProvidersDoctorCommand(opts) {
73614
75652
  const registry = opts?.registry ?? DEFAULT_LOCAL_PROVIDER_REGISTRY;
73615
75653
  const write = opts?.write ?? ((line2) => console.log(line2));
73616
- const results = buildProviderResults(registry);
73617
- write(import_picocolors30.default.bold("\nLocal AI providers\n"));
75654
+ const results = buildProviderResults(registry, opts?._detect);
75655
+ write(import_picocolors31.default.bold("\nLocal AI providers\n"));
73618
75656
  for (const result of results) {
73619
75657
  const descriptor = registry.get(result.id);
73620
75658
  const name = descriptor?.displayName ?? result.id;
@@ -73622,12 +75660,24 @@ async function runProvidersDoctorCommand(opts) {
73622
75660
  ` ${statusMark(result.status)} ${result.id.padEnd(8)} ${name.padEnd(22)} ${statusLabel(result.status)}`
73623
75661
  );
73624
75662
  if (result.status !== "ready" && result.detail) {
73625
- write(` ${import_picocolors30.default.dim("\u2192 " + result.detail)}`);
75663
+ write(` ${import_picocolors31.default.dim("\u2192 " + result.detail)}`);
73626
75664
  }
73627
75665
  }
73628
75666
  write("");
73629
- write(import_picocolors30.default.dim(" Detection not yet implemented \u2014 install the provider binary first."));
73630
- write(import_picocolors30.default.dim(' Cloud provider: ANTHROPIC_API_KEY=<key> horus investigate "hint" --ai'));
75667
+ write(import_picocolors31.default.bold("Cloud AI providers\n"));
75668
+ const anthropicKey = opts?._anthropicKey !== void 0 ? opts._anthropicKey : process.env["ANTHROPIC_API_KEY"] ?? null;
75669
+ if (anthropicKey) {
75670
+ write(
75671
+ ` ${import_picocolors31.default.green("\u2713")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors31.default.green("ANTHROPIC_API_KEY configured")}`
75672
+ );
75673
+ } else {
75674
+ write(
75675
+ ` ${import_picocolors31.default.red("\u2717")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors31.default.dim("ANTHROPIC_API_KEY not set")}`
75676
+ );
75677
+ write(
75678
+ ` ${import_picocolors31.default.dim('\u2192 set ANTHROPIC_API_KEY=<key> to enable AI-powered investigation (horus investigate "hint" --ai)')}`
75679
+ );
75680
+ }
73631
75681
  write("");
73632
75682
  return 0;
73633
75683
  }
@@ -73636,7 +75686,7 @@ async function runProvidersDoctorCommand(opts) {
73636
75686
  init_cjs_shims();
73637
75687
  var import_node_fs8 = require("fs");
73638
75688
  var import_node_path10 = require("path");
73639
- var import_picocolors31 = __toESM(require_picocolors(), 1);
75689
+ var import_picocolors32 = __toESM(require_picocolors(), 1);
73640
75690
  function configTemplate(name, repoPath) {
73641
75691
  return `export default {
73642
75692
  database: {
@@ -73662,39 +75712,66 @@ function configTemplate(name, repoPath) {
73662
75712
  };
73663
75713
  `;
73664
75714
  }
75715
+ function projectDefaults(localConfigPath2) {
75716
+ try {
75717
+ const file = readLocalConfig(localConfigPath2);
75718
+ const project = file.project;
75719
+ if (!project) return null;
75720
+ const name = typeof project.name === "string" ? project.name : null;
75721
+ const repositories2 = Array.isArray(project.repositories) ? project.repositories : [];
75722
+ const firstRepo = repositories2[0];
75723
+ const repoPath = firstRepo && typeof firstRepo.path === "string" ? firstRepo.path : null;
75724
+ if (name == null) return null;
75725
+ return { name, repoPath: repoPath ?? name };
75726
+ } catch {
75727
+ return null;
75728
+ }
75729
+ }
73665
75730
  async function runGenerateConfig(opts) {
73666
75731
  const log = opts.write ?? ((line2) => console.log(line2));
73667
75732
  const cwd = opts.cwd ?? process.cwd();
73668
- const outPath = (0, import_node_path10.resolve)(cwd, opts.out ?? "horus.config.js");
73669
- const name = opts.name ?? "my-project";
73670
- const repoPath = opts.repo ?? `/path/to/${name}`;
75733
+ const localConfigPath2 = discoverLocalConfig(cwd);
75734
+ const defaults = localConfigPath2 != null ? projectDefaults(localConfigPath2) : null;
75735
+ const hasLocalConfig = defaults != null;
75736
+ const defaultOut = hasLocalConfig ? "horus.config.example.js" : "horus.config.js";
75737
+ const outPath = (0, import_node_path10.resolve)(cwd, opts.out ?? defaultOut);
75738
+ const name = opts.name ?? defaults?.name ?? "my-project";
75739
+ const repoPath = opts.repo ?? defaults?.repoPath ?? `/path/to/${name}`;
73671
75740
  if ((0, import_node_fs8.existsSync)(outPath) && !opts.force) {
73672
- log(`${import_picocolors31.default.red("\u2717")} ${outPath} already exists`);
73673
- log(import_picocolors31.default.dim(" pass --force to overwrite"));
75741
+ log(`${import_picocolors32.default.red("\u2717")} ${outPath} already exists`);
75742
+ log(import_picocolors32.default.dim(" pass --force to overwrite"));
73674
75743
  return 1;
73675
75744
  }
73676
75745
  try {
73677
75746
  (0, import_node_fs8.mkdirSync)((0, import_node_path10.dirname)(outPath), { recursive: true });
73678
75747
  (0, import_node_fs8.writeFileSync)(outPath, configTemplate(name, repoPath), "utf8");
73679
75748
  } catch (err) {
73680
- log(`${import_picocolors31.default.red("\u2717")} Could not write ${outPath}: ${err.message}`);
75749
+ log(`${import_picocolors32.default.red("\u2717")} Could not write ${outPath}: ${err.message}`);
73681
75750
  return 1;
73682
75751
  }
73683
- log(`${import_picocolors31.default.green("\u2713")} Created ${outPath}`);
73684
- log(import_picocolors31.default.dim(` project: ${name}`));
73685
- log(import_picocolors31.default.dim(` repo: ${repoPath}`));
73686
- log(import_picocolors31.default.dim(` next: horus doctor --config ${outPath}`));
75752
+ log(`${import_picocolors32.default.green("\u2713")} Created ${outPath}`);
75753
+ log(import_picocolors32.default.dim(` project: ${name}`));
75754
+ log(import_picocolors32.default.dim(` repo: ${repoPath}`));
75755
+ if (hasLocalConfig && localConfigPath2 != null) {
75756
+ log("");
75757
+ log(import_picocolors32.default.yellow("Note:") + ` an initialized Horus project config exists at ${localConfigPath2}`);
75758
+ log(" \u2022 .horus/config.json \u2014 project config used by `horus investigate` from this repo");
75759
+ log(" \u2022 horus.config.js \u2014 standalone/global config used with `horus doctor --config <path>`");
75760
+ log(import_picocolors32.default.dim(` next: review ${outPath} and copy/adapt it as needed`));
75761
+ } else {
75762
+ log(import_picocolors32.default.dim(` next: horus doctor --config ${outPath}`));
75763
+ }
73687
75764
  return 0;
73688
75765
  }
73689
75766
 
73690
75767
  // ../../packages/cli/src/commands/readiness.ts
73691
75768
  init_cjs_shims();
73692
- var import_picocolors32 = __toESM(require_picocolors(), 1);
75769
+ var import_picocolors33 = __toESM(require_picocolors(), 1);
73693
75770
  var DEFAULT_DB_URL4 = "postgresql://horus:horus@localhost:5433/horus";
73694
75771
  function mark3(status) {
73695
- if (status === "pass") return import_picocolors32.default.green("\u2713");
73696
- if (status === "warn") return import_picocolors32.default.yellow("~");
73697
- return import_picocolors32.default.red("\u2717");
75772
+ if (status === "pass") return import_picocolors33.default.green("\u2713");
75773
+ if (status === "warn") return import_picocolors33.default.yellow("~");
75774
+ return import_picocolors33.default.red("\u2717");
73698
75775
  }
73699
75776
  async function runReadiness(opts) {
73700
75777
  const cwd = opts?.cwd ?? process.cwd();
@@ -73757,7 +75834,7 @@ async function runReadiness(opts) {
73757
75834
  status: "warn",
73758
75835
  blocking: false,
73759
75836
  detail: "not installed \u2014 source intelligence unavailable",
73760
- next: `uv tool install axoniq==${PINNED_SOURCE_VERSION}`
75837
+ next: `pip install horus-source`
73761
75838
  });
73762
75839
  } else if (sourceVersion !== PINNED_SOURCE_VERSION) {
73763
75840
  checks.push({
@@ -73765,7 +75842,7 @@ async function runReadiness(opts) {
73765
75842
  status: "warn",
73766
75843
  blocking: false,
73767
75844
  detail: `version mismatch (installed: ${sourceVersion}, required: ${PINNED_SOURCE_VERSION})`,
73768
- next: `uv tool install axoniq==${PINNED_SOURCE_VERSION}`
75845
+ next: `pip install horus-source`
73769
75846
  });
73770
75847
  } else {
73771
75848
  checks.push({
@@ -73849,20 +75926,20 @@ async function runReadiness(opts) {
73849
75926
  }
73850
75927
  const blockingChecks = checks.filter((c) => c.blocking);
73851
75928
  const optionalChecks = checks.filter((c) => !c.blocking);
73852
- write(import_picocolors32.default.bold("\nHorus release readiness\n"));
73853
- write(import_picocolors32.default.bold(" Blocking"));
75929
+ write(import_picocolors33.default.bold("\nHorus release readiness\n"));
75930
+ write(import_picocolors33.default.bold(" Blocking"));
73854
75931
  for (const check of blockingChecks) {
73855
- write(` ${mark3(check.status)} ${import_picocolors32.default.bold(check.label.padEnd(22))} ${import_picocolors32.default.dim(check.detail)}`);
75932
+ write(` ${mark3(check.status)} ${import_picocolors33.default.bold(check.label.padEnd(22))} ${import_picocolors33.default.dim(check.detail)}`);
73856
75933
  if (check.next) {
73857
- write(` ${import_picocolors32.default.dim("\u2192 " + check.next)}`);
75934
+ write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
73858
75935
  }
73859
75936
  }
73860
75937
  write("");
73861
- write(import_picocolors32.default.bold(" Optional"));
75938
+ write(import_picocolors33.default.bold(" Optional"));
73862
75939
  for (const check of optionalChecks) {
73863
- write(` ${mark3(check.status)} ${import_picocolors32.default.bold(check.label.padEnd(22))} ${import_picocolors32.default.dim(check.detail)}`);
75940
+ write(` ${mark3(check.status)} ${import_picocolors33.default.bold(check.label.padEnd(22))} ${import_picocolors33.default.dim(check.detail)}`);
73864
75941
  if (check.next) {
73865
- write(` ${import_picocolors32.default.dim("\u2192 " + check.next)}`);
75942
+ write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
73866
75943
  }
73867
75944
  }
73868
75945
  write("");
@@ -73870,21 +75947,21 @@ async function runReadiness(opts) {
73870
75947
  const optionalWarns = optionalChecks.filter((c) => c.status === "warn").length;
73871
75948
  if (blockingFails.length === 0) {
73872
75949
  if (optionalWarns === 0) {
73873
- write(import_picocolors32.default.green(" Ready for demo/release."));
75950
+ write(import_picocolors33.default.green(" Ready for demo/release."));
73874
75951
  } else {
73875
75952
  write(
73876
- import_picocolors32.default.yellow(
75953
+ import_picocolors33.default.yellow(
73877
75954
  ` Ready for a basic demo. ${optionalWarns} optional item(s) not configured \u2014 investigation evidence will be limited.`
73878
75955
  )
73879
75956
  );
73880
75957
  }
73881
75958
  } else {
73882
75959
  write(
73883
- import_picocolors32.default.red(
75960
+ import_picocolors33.default.red(
73884
75961
  ` Not ready. ${blockingFails.length} blocking item(s) must be resolved before demo/release.`
73885
75962
  )
73886
75963
  );
73887
- write(import_picocolors32.default.dim(" Re-run `horus readiness` after resolving the items above."));
75964
+ write(import_picocolors33.default.dim(" Re-run `horus readiness` after resolving the items above."));
73888
75965
  }
73889
75966
  write("");
73890
75967
  return blockingFails.length > 0 ? 1 : 0;
@@ -73935,7 +76012,7 @@ Examples:
73935
76012
  });
73936
76013
  program2.command("connect <type>").description(
73937
76014
  "Add or update a runtime connector (elasticsearch / mongodb / grafana / redis) in .horus/config.json"
73938
- ).option("--env <name>", "target environment (default: first environment in config)").option("--url <url>", "connector URL or connection string").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--dashboard <uid>", "default dashboard uid (grafana)").option("--no-test", "skip live connection probe").action(
76015
+ ).option("--env <name>", "target environment (default: first environment in config)").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--dashboard <uid>", "default dashboard uid (grafana)").option("--no-test", "skip live connection probe").action(
73939
76016
  async (type, opts) => {
73940
76017
  process.exitCode = await runConnect(type, {
73941
76018
  env: opts.env,
@@ -73984,9 +76061,16 @@ Examples:
73984
76061
  process.exitCode = await runIndex(opts);
73985
76062
  }
73986
76063
  );
73987
- program2.command("queues [name]").description("Show producer -> queue -> worker edges").option("-c, --config <path>", "path to horus.config.ts").option("--project <name>", "filter edges by project").action(async (name, opts) => {
73988
- process.exitCode = await runQueues(name, { config: opts.config, project: opts.project });
73989
- });
76064
+ program2.command("queues [name]").description("Show queue topology from source intelligence; --live adds real-time Redis/BullMQ state").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via registry)").option("--project <name>", "filter edges by project").option("--live", "fetch real-time queue depths and failed-job counts from Redis/BullMQ").action(
76065
+ async (name, opts) => {
76066
+ process.exitCode = await runQueues(name, {
76067
+ config: opts.config,
76068
+ name: opts.name,
76069
+ project: opts.project,
76070
+ live: opts.live
76071
+ });
76072
+ }
76073
+ );
73990
76074
  program2.command("investigate <hint>").description("Run a deterministic investigation for an incident hint").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name to scope to").option("--env <name>", "environment name (e.g. production)").option("--repo <name>", "repository/project to scope to (alias for --project)").option("--since <ref>", "git ref/range for change-impact (e.g. HEAD~5)").option(
73991
76075
  "--service <name>",
73992
76076
  "service name to scope runtime logs, e.g. leadcall-api-prod"
@@ -74081,8 +76165,8 @@ Examples:
74081
76165
  horus investigations
74082
76166
  horus investigations -n 20
74083
76167
  `);
74084
- program2.command("replay <id>").description("Re-render a saved investigation from the audit store (no re-query)").option("-c, --config <path>", "path to horus.config.ts").option("--format <fmt>", "text | markdown | json", "text").option("--ai", "enrich report with AI narrative (requires ANTHROPIC_API_KEY; falls back to deterministic on failure)").option("--ai-model <model>", "AI model for --ai (default: claude-opus-4-8)").action(async (id, opts) => {
74085
- process.exitCode = await runReplay(id, { config: opts.config, format: opts.format, ai: opts.ai, aiModel: opts.aiModel });
76168
+ program2.command("replay <id>").description("Re-render a saved investigation from the audit store (no re-query)").option("-c, --config <path>", "path to horus.config.ts").option("--format <fmt>", "text | markdown | json", "text").option("--ai", "enrich report with AI narrative (requires ANTHROPIC_API_KEY; falls back to deterministic on failure)").option("--ai-model <model>", "AI model for --ai (default: claude-opus-4-8)").option("--refresh-ai", "re-run AI even if a stored judgment already exists").action(async (id, opts) => {
76169
+ process.exitCode = await runReplay(id, { config: opts.config, format: opts.format, ai: opts.ai, aiModel: opts.aiModel, refreshAi: opts.refreshAi });
74086
76170
  }).addHelpText("after", `
74087
76171
  Examples:
74088
76172
  horus replay <id>
@@ -74090,11 +76174,12 @@ Examples:
74090
76174
  horus replay <id> --format json
74091
76175
  horus replay <id> --ai
74092
76176
  horus replay <id> --ai --ai-model claude-sonnet-4-6
76177
+ horus replay <id> --ai --refresh-ai
74093
76178
 
74094
76179
  (Use 'horus investigations' to list saved investigation ids.)
74095
76180
  `);
74096
- program2.command("postmortem <id>").description("Draft an editable incident postmortem from a saved investigation").option("-c, --config <path>", "path to horus.config.ts").option("--output <path>", "write Markdown to a file instead of printing to stdout").option("--force", "overwrite the output file if it already exists").option("--ai-summary", "append an AI-generated summary section (requires ANTHROPIC_API_KEY; falls back gracefully)").option("--ai-model <model>", "AI model for --ai-summary (default: claude-opus-4-8)").action(async (id, opts) => {
74097
- process.exitCode = await runPostmortem(id, { config: opts.config, output: opts.output, force: opts.force, aiSummary: opts.aiSummary, aiModel: opts.aiModel });
76181
+ program2.command("postmortem <id>").description("Draft an editable incident postmortem from a saved investigation").option("-c, --config <path>", "path to horus.config.ts").option("--output <path>", "write Markdown to a file instead of printing to stdout").option("--force", "overwrite the output file if it already exists").option("--ai-summary", "append an AI-generated summary section (requires ANTHROPIC_API_KEY; falls back gracefully)").option("--ai-model <model>", "AI model for --ai-summary (default: claude-opus-4-8)").option("--refresh-ai", "re-run AI even if a stored judgment already exists").action(async (id, opts) => {
76182
+ process.exitCode = await runPostmortem(id, { config: opts.config, output: opts.output, force: opts.force, aiSummary: opts.aiSummary, aiModel: opts.aiModel, refreshAi: opts.refreshAi });
74098
76183
  }).addHelpText("after", `
74099
76184
  Examples:
74100
76185
  horus postmortem <id>
@@ -74102,6 +76187,7 @@ Examples:
74102
76187
  horus postmortem <id> --output ./postmortem.md --force
74103
76188
  horus postmortem <id> --ai-summary
74104
76189
  horus postmortem <id> --output ./postmortem.md --ai-summary --ai-model claude-sonnet-4-6
76190
+ horus postmortem <id> --ai-summary --refresh-ai
74105
76191
 
74106
76192
  (Use 'horus investigations' to list saved investigation ids.)
74107
76193
  `);