@merittdev/horus 0.1.1 → 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 (3) hide show
  1. package/README.md +100 -116
  2. package/dist/index.cjs +2635 -459
  3. 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.1";
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)) {
@@ -67040,6 +67306,22 @@ async function runIndex(opts) {
67040
67306
  ` investigate: horus investigate --name ${name} "<hint>" (or from this repo: horus investigate "<hint>")`
67041
67307
  )
67042
67308
  );
67309
+ } else if (spawned && !configuredHost) {
67310
+ const existingPath = discoverLocalConfig(root);
67311
+ if (existingPath) {
67312
+ const file = readLocalConfig(existingPath);
67313
+ const project = file.project;
67314
+ const repos = project["repositories"];
67315
+ if (repos && repos.length > 0) {
67316
+ repos[0]["source"] = { hostUrl };
67317
+ }
67318
+ writeLocalConfig(root, file);
67319
+ registerProject(label, root, existingPath);
67320
+ console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} \u2014 source host registered at ${hostUrl}`);
67321
+ console.log(import_picocolors3.default.dim(` ${existingPath}`));
67322
+ } else {
67323
+ console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`);
67324
+ }
67043
67325
  } else {
67044
67326
  console.log(
67045
67327
  `${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`
@@ -67057,64 +67339,29 @@ init_cjs_shims();
67057
67339
  var import_picocolors4 = __toESM(require_picocolors(), 1);
67058
67340
  async function runQueues(name, opts) {
67059
67341
  try {
67060
- const config = await loadConfig(opts.config);
67342
+ const config = await loadConfig(opts.config, { name: opts.name });
67061
67343
  const { db, sql: sql2 } = createDb(config.database.url);
67062
67344
  try {
67063
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("");
67064
67350
  if (rows.length === 0) {
67065
- console.log("No queue edges. Run: horus index");
67066
- 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);
67067
67355
  }
67068
- const byQueue = /* @__PURE__ */ new Map();
67069
- for (const row of rows) {
67070
- const existing = byQueue.get(row.queueName);
67071
- if (existing) {
67072
- existing.push(row);
67073
- } else {
67074
- byQueue.set(row.queueName, [row]);
67075
- }
67076
- }
67077
- for (const [queueName, edges] of byQueue) {
67078
- console.log(import_picocolors4.default.bold(queueName));
67079
- const producerSet = /* @__PURE__ */ new Set();
67080
- const producerDetails = /* @__PURE__ */ new Map();
67081
- for (const edge of edges) {
67082
- if (edge.producerSymbol) {
67083
- producerSet.add(edge.producerSymbol);
67084
- if (edge.producerFile) {
67085
- producerDetails.set(edge.producerSymbol, edge.producerFile);
67086
- }
67087
- }
67088
- }
67089
- if (producerSet.size === 0) {
67090
- console.log(" producers: " + import_picocolors4.default.dim("none"));
67091
- } else {
67092
- const producerList = Array.from(producerSet).map((sym) => {
67093
- const file = producerDetails.get(sym);
67094
- return file ? `${sym} (${file})` : sym;
67095
- }).join(", ");
67096
- console.log(" producers: " + producerList);
67097
- }
67098
- const workerSet = /* @__PURE__ */ new Set();
67099
- const workerDetails = /* @__PURE__ */ new Map();
67100
- for (const edge of edges) {
67101
- if (edge.workerSymbol) {
67102
- workerSet.add(edge.workerSymbol);
67103
- if (edge.workerFile) {
67104
- workerDetails.set(edge.workerSymbol, edge.workerFile);
67105
- }
67106
- }
67107
- }
67108
- if (workerSet.size === 0) {
67109
- console.log(" workers: " + import_picocolors4.default.dim("none"));
67110
- } else {
67111
- const workerList = Array.from(workerSet).map((sym) => {
67112
- const file = workerDetails.get(sym);
67113
- return file ? `${sym} (${file})` : sym;
67114
- }).join(", ");
67115
- console.log(" workers: " + workerList);
67116
- }
67117
- 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
+ );
67118
67365
  }
67119
67366
  } finally {
67120
67367
  await sql2.end();
@@ -67125,6 +67372,141 @@ async function runQueues(name, opts) {
67125
67372
  return 1;
67126
67373
  }
67127
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
+ }
67128
67510
 
67129
67511
  // ../../packages/cli/src/commands/investigate.ts
67130
67512
  init_cjs_shims();
@@ -67141,8 +67523,96 @@ init_cjs_shims();
67141
67523
 
67142
67524
  // ../../packages/engine/src/ownership.ts
67143
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
+ }
67144
67579
  async function estimateOwnership(query, deps) {
67145
- 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
+ }
67146
67616
  const file = top?.filePath ?? null;
67147
67617
  if (file === null) {
67148
67618
  return {
@@ -67266,7 +67736,48 @@ function processEvidence(ev, nodes, edges) {
67266
67736
  addEdge(edges, "observed_in", evId, deployId, ev.id);
67267
67737
  return;
67268
67738
  }
67269
- // 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
67270
67781
  default:
67271
67782
  return;
67272
67783
  }
@@ -67287,18 +67798,28 @@ function scoreImplication(nodes, edges, evidence2) {
67287
67798
  const evById = new Map(evidence2.map((e) => [e.id, e]));
67288
67799
  for (const node of nodes.values()) {
67289
67800
  if (node.type === "evidence") continue;
67801
+ if (node.type === "symbol" || node.type === "file" || node.type === "flow") continue;
67290
67802
  node.implicationScore = node.evidenceIds.reduce((max, eid) => {
67291
67803
  const ev = evById.get(eid);
67292
67804
  if (!ev || isExcludedFromImplication(ev)) return max;
67293
67805
  return Math.max(max, ev.relevance);
67294
67806
  }, 0);
67295
67807
  }
67808
+ const CODE_TOPOLOGY_NODE_TYPES = /* @__PURE__ */ new Set([
67809
+ "evidence",
67810
+ "symbol",
67811
+ "file",
67812
+ "flow"
67813
+ ]);
67296
67814
  const adj = /* @__PURE__ */ new Map();
67297
67815
  for (const edge of edges.values()) {
67298
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;
67299
67820
  const f = nodes.get(edge.from);
67300
67821
  const t = nodes.get(edge.to);
67301
- 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;
67302
67823
  const fs3 = adj.get(edge.from) ?? /* @__PURE__ */ new Set();
67303
67824
  const ts = adj.get(edge.to) ?? /* @__PURE__ */ new Set();
67304
67825
  fs3.add(edge.to);
@@ -67323,6 +67844,7 @@ function scoreImplication(nodes, edges, evidence2) {
67323
67844
  }
67324
67845
  for (const node of nodes.values()) {
67325
67846
  if (node.type === "evidence") continue;
67847
+ if (node.type === "symbol" || node.type === "file" || node.type === "flow") continue;
67326
67848
  node.implicated = node.implicationScore >= IMPLICATION_THRESHOLD;
67327
67849
  }
67328
67850
  }
@@ -67354,6 +67876,302 @@ function implicatedNodeIds(graph, evidenceIds) {
67354
67876
  (n) => n.type !== "evidence" && n.implicated && n.evidenceIds.some((eid) => idSet.has(eid))
67355
67877
  ).map((n) => n.id);
67356
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
+ }
67357
68175
 
67358
68176
  // ../../packages/engine/src/score-cause.ts
67359
68177
  init_cjs_shims();
@@ -67435,20 +68253,23 @@ function factorRuntimeSignals(items, now) {
67435
68253
  const timestamps = items.filter((e) => e.timestamp !== void 0).map((e) => new Date(e.timestamp).getTime()).sort((a, b2) => b2 - a);
67436
68254
  const newestTs = timestamps[0];
67437
68255
  if (newestTs !== void 0) {
67438
- const ageMs = nowMs - newestTs;
67439
- if (ageMs <= 36e5) {
67440
- recencyDelta = 0.05;
67441
- recencyReason = "Evidence from within the last hour";
67442
- } else if (ageMs <= 864e5) {
67443
- recencyDelta = 0.02;
67444
- recencyReason = "Evidence from within the last 24 hours";
67445
- } else if (ageMs <= 2592e5) {
67446
- } else if (ageMs <= 6048e5) {
67447
- recencyDelta = -0.02;
67448
- recencyReason = "Most recent evidence is 3\u20137 days old \u2014 may predate this incident";
67449
- } else {
67450
- recencyDelta = -0.05;
67451
- recencyReason = "Most recent evidence is over 7 days old \u2014 likely predates this incident";
68256
+ const rawAgeMs = nowMs - newestTs;
68257
+ const ageMs = rawAgeMs < 0 ? rawAgeMs >= -3e5 ? 0 : null : rawAgeMs;
68258
+ if (ageMs !== null) {
68259
+ if (ageMs <= 36e5) {
68260
+ recencyDelta = 0.05;
68261
+ recencyReason = "Evidence from within the last hour";
68262
+ } else if (ageMs <= 864e5) {
68263
+ recencyDelta = 0.02;
68264
+ recencyReason = "Evidence from within the last 24 hours";
68265
+ } else if (ageMs <= 2592e5) {
68266
+ } else if (ageMs <= 6048e5) {
68267
+ recencyDelta = -0.02;
68268
+ recencyReason = "Most recent evidence is 3\u20137 days old \u2014 may predate this incident";
68269
+ } else {
68270
+ recencyDelta = -0.05;
68271
+ recencyReason = "Most recent evidence is over 7 days old \u2014 likely predates this incident";
68272
+ }
67452
68273
  }
67453
68274
  }
67454
68275
  let recurrenceDelta = 0;
@@ -67613,6 +68434,15 @@ function generateHypotheses(evidence2, _correlation, ctx) {
67613
68434
  const queueEvs = evidence2.filter((e) => e.kind === "queue-edge");
67614
68435
  const hasCommit = commitEvs.length > 0;
67615
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") : [];
67616
68446
  const hyps = [];
67617
68447
  hyps.push({
67618
68448
  id: globalThis.crypto.randomUUID(),
@@ -67621,9 +68451,7 @@ function generateHypotheses(evidence2, _correlation, ctx) {
67621
68451
  confidence: hasCommit ? 0.5 : 0.15,
67622
68452
  supportingEvidenceIds: commitEvs.map((e) => e.id),
67623
68453
  contradictingEvidenceIds: [],
67624
- missingEvidence: hasCommit ? [] : [
67625
- "A change/deployment range \u2014 re-run with --since <ref> to diff what shipped"
67626
- ]
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"]
67627
68455
  });
67628
68456
  for (const queueName of queues) {
67629
68457
  const backlogEvIds = ctx.queueBacklogEvIdsByQueue?.get(queueName) ?? [];
@@ -67659,25 +68487,32 @@ function generateHypotheses(evidence2, _correlation, ctx) {
67659
68487
  contradictingEvidenceIds: [],
67660
68488
  missingEvidence: latencyMetricEvIds.length > 0 ? [] : ["Request latency metrics (Grafana) + error logs (Elasticsearch)"]
67661
68489
  });
68490
+ const retryStormSupport = [.../* @__PURE__ */ new Set([...logSpikeEvIds, ...allQueueBacklogEvIds])];
67662
68491
  hyps.push({
67663
68492
  id: globalThis.crypto.randomUUID(),
67664
68493
  category: "retry-storm",
67665
68494
  statement: "A retry storm is amplifying load on the failing path.",
67666
- confidence: 0.15,
67667
- supportingEvidenceIds: [],
67668
- contradictingEvidenceIds: [],
67669
- missingEvidence: [
67670
- "Retry/error logs + queue retry statistics (Elasticsearch + BullMQ)"
67671
- ]
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)"]
67672
68499
  });
68500
+ const infraSupport = [
68501
+ .../* @__PURE__ */ new Set([
68502
+ ...stateEvIds,
68503
+ ...allQueueStarvationEvIds,
68504
+ ...ctx.latencyMetricEvIds ?? [],
68505
+ ...graphImplicatedCollectionEvIds
68506
+ ])
68507
+ ];
67673
68508
  hyps.push({
67674
68509
  id: globalThis.crypto.randomUUID(),
67675
68510
  category: "infrastructure",
67676
68511
  statement: "An infrastructure issue (database, Redis, or network) is degrading processing.",
67677
- confidence: 0.15,
67678
- supportingEvidenceIds: [],
68512
+ confidence: infraSupport.length > 0 ? 0.35 : 0.15,
68513
+ supportingEvidenceIds: infraSupport,
67679
68514
  contradictingEvidenceIds: [],
67680
- missingEvidence: ["Infra/Redis metrics (Grafana) + Redis state"]
68515
+ missingEvidence: infraSupport.length > 0 ? [] : ["Infra/Redis metrics (Grafana) + Redis state"]
67681
68516
  });
67682
68517
  hyps.sort((a, b2) => b2.confidence - a.confidence);
67683
68518
  return hyps;
@@ -67898,7 +68733,8 @@ function detectMissingEvidence(r, connectors = {}) {
67898
68733
  blindSpots.push("Cannot see the real error.");
67899
68734
  }
67900
68735
  if (!hasMetric && !(connectors.grafana && connectors.metricsCollected)) {
67901
- 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.`;
67902
68738
  const metricsNextSource = !connectors.grafana ? "Add a `grafana` connector to the environment" : 'Check Grafana connectivity, then run `horus metrics "<hint>"` manually';
67903
68739
  gaps.push({
67904
68740
  dimension: "metrics",
@@ -67912,7 +68748,7 @@ function detectMissingEvidence(r, connectors = {}) {
67912
68748
  gaps.push({
67913
68749
  dimension: "queue runtime state",
67914
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.",
67915
- 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",
67916
68752
  confidenceImpact: 0.1
67917
68753
  });
67918
68754
  blindSpots.push("Cannot determine if the queue is actually backed up.");
@@ -67920,8 +68756,8 @@ function detectMissingEvidence(r, connectors = {}) {
67920
68756
  if (!hasCommit) {
67921
68757
  gaps.push({
67922
68758
  dimension: "deployment records",
67923
- why: "No deployment/change data in scope \u2014 cannot tell what shipped before the incident.",
67924
- 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>`",
67925
68761
  confidenceImpact: 0.08
67926
68762
  });
67927
68763
  blindSpots.push("Cannot correlate with a recent change.");
@@ -68098,33 +68934,33 @@ function buildGroups(evidence2) {
68098
68934
  }
68099
68935
  }
68100
68936
  const raw = [];
68101
- for (const [key, ids] of symbolMap) {
68102
- if (ids.length >= 2) {
68937
+ for (const [key, ids2] of symbolMap) {
68938
+ if (ids2.length >= 2) {
68103
68939
  raw.push({
68104
68940
  key,
68105
68941
  dimension: "symbol",
68106
68942
  reason: `Share symbol ${key}`,
68107
- evidenceIds: [...ids]
68943
+ evidenceIds: [...ids2]
68108
68944
  });
68109
68945
  }
68110
68946
  }
68111
- for (const [key, ids] of fileMap) {
68112
- if (ids.length >= 2) {
68947
+ for (const [key, ids2] of fileMap) {
68948
+ if (ids2.length >= 2) {
68113
68949
  raw.push({
68114
68950
  key,
68115
68951
  dimension: "file",
68116
68952
  reason: `Share file ${key}`,
68117
- evidenceIds: [...ids]
68953
+ evidenceIds: [...ids2]
68118
68954
  });
68119
68955
  }
68120
68956
  }
68121
- for (const [key, ids] of queueMap) {
68122
- if (ids.length >= 2) {
68957
+ for (const [key, ids2] of queueMap) {
68958
+ if (ids2.length >= 2) {
68123
68959
  raw.push({
68124
68960
  key,
68125
68961
  dimension: "queue",
68126
68962
  reason: `Share queue ${key}`,
68127
- evidenceIds: [...ids]
68963
+ evidenceIds: [...ids2]
68128
68964
  });
68129
68965
  }
68130
68966
  }
@@ -68148,13 +68984,13 @@ function buildChains(evidence2) {
68148
68984
  const commitEvs = evidence2.filter((e) => e.kind === "commit");
68149
68985
  const symbolEvs = evidence2.filter((e) => e.kind === "symbol");
68150
68986
  const relevanceById = new Map(evidence2.map((e) => [e.id, e.relevance]));
68151
- function avgRelevance(ids) {
68152
- if (ids.length === 0) return 0;
68987
+ function avgRelevance(ids2) {
68988
+ if (ids2.length === 0) return 0;
68153
68989
  let sum = 0;
68154
- for (const id of ids) {
68990
+ for (const id of ids2) {
68155
68991
  sum += relevanceById.get(id) ?? 0;
68156
68992
  }
68157
- return sum / ids.length;
68993
+ return sum / ids2.length;
68158
68994
  }
68159
68995
  const chains = [];
68160
68996
  if (queueEdges2.length > 0) {
@@ -68258,7 +69094,7 @@ function seedRole(s) {
68258
69094
  if (/util|helper/i.test(hay)) return "util";
68259
69095
  return "code";
68260
69096
  }
68261
- function scoreSeed(s, index2) {
69097
+ function scoreSeed(s, index2, hintTokens) {
68262
69098
  const hay = `${s.name} ${s.filePath}`.toLowerCase();
68263
69099
  let score = 0;
68264
69100
  if (PREFER.test(hay)) score += 3;
@@ -68266,10 +69102,20 @@ function scoreSeed(s, index2) {
68266
69102
  if (/\.(resolver|controller|service)\.[jt]sx?$/i.test(s.filePath)) score += 2;
68267
69103
  if (/(^|\/)scripts?\//i.test(s.filePath)) score -= 2;
68268
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
+ }
68269
69115
  return score;
68270
69116
  }
68271
- function rankSeeds(seeds) {
68272
- 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 }));
68273
69119
  }
68274
69120
 
68275
69121
  // ../../packages/engine/src/normalize.ts
@@ -68329,6 +69175,7 @@ var RUNTIME_KINDS2 = /* @__PURE__ */ new Set([
68329
69175
  ]);
68330
69176
  var MAX_RUNTIME_CONTRIBUTION = 2;
68331
69177
  var MAX_STRUCTURAL_CONTRIBUTION = 0.6;
69178
+ var MAX_AMBIENT_RUNTIME_CONTRIBUTION = 0.6;
68332
69179
  var NORMALIZATION = 6;
68333
69180
  function clamp014(n) {
68334
69181
  if (Number.isNaN(n)) return 0;
@@ -68336,13 +69183,18 @@ function clamp014(n) {
68336
69183
  if (n > 1) return 1;
68337
69184
  return n;
68338
69185
  }
68339
- function computeWeightedEvidenceConfidence(evidence2) {
69186
+ function computeWeightedEvidenceConfidence(evidence2, ambientEvidenceIds) {
68340
69187
  const runtimeBySource = /* @__PURE__ */ new Map();
69188
+ const ambientBySource = /* @__PURE__ */ new Map();
68341
69189
  const structuralBySource = /* @__PURE__ */ new Map();
68342
69190
  for (const e of evidence2) {
68343
69191
  const r = clamp014(e.relevance);
68344
69192
  if (RUNTIME_KINDS2.has(e.kind)) {
68345
- 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
+ }
68346
69198
  } else {
68347
69199
  structuralBySource.set(e.source, (structuralBySource.get(e.source) ?? 0) + 0.5 * r);
68348
69200
  }
@@ -68351,11 +69203,15 @@ function computeWeightedEvidenceConfidence(evidence2) {
68351
69203
  (acc, w) => acc + Math.min(w, MAX_RUNTIME_CONTRIBUTION),
68352
69204
  0
68353
69205
  );
69206
+ const ambientSum = [...ambientBySource.values()].reduce(
69207
+ (acc, w) => acc + Math.min(w, MAX_AMBIENT_RUNTIME_CONTRIBUTION),
69208
+ 0
69209
+ );
68354
69210
  const structuralSum = [...structuralBySource.values()].reduce(
68355
69211
  (acc, w) => acc + Math.min(w, MAX_STRUCTURAL_CONTRIBUTION),
68356
69212
  0
68357
69213
  );
68358
- return clamp014((runtimeSum + structuralSum) / NORMALIZATION);
69214
+ return clamp014((runtimeSum + ambientSum + structuralSum) / NORMALIZATION);
68359
69215
  }
68360
69216
 
68361
69217
  // ../../packages/engine/src/git-collector.ts
@@ -68371,8 +69227,9 @@ function isRefLike(s) {
68371
69227
  if (t.startsWith("-")) return false;
68372
69228
  if (t.includes("..")) return false;
68373
69229
  if (/\s|:|ago|yesterday|week|month|year|=/i.test(t)) return false;
69230
+ if (/^\d+[smhd]$/i.test(t)) return false;
68374
69231
  if (/^[0-9a-f]{7,40}$/i.test(t)) return true;
68375
- 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;
68376
69233
  return false;
68377
69234
  }
68378
69235
  function parseDiffStat(stdout) {
@@ -68591,6 +69448,33 @@ function logWindowFrom(since) {
68591
69448
  }
68592
69449
  return new Date(now - 7 * 864e5).toISOString();
68593
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
+ }
68594
69478
  function queueFindingConfidence(opts) {
68595
69479
  const { starvedCount, backloggedCount, failingCount } = opts;
68596
69480
  return starvedCount > 0 && backloggedCount === 0 && failingCount === 0 ? 0.65 : 0.85;
@@ -68599,7 +69483,8 @@ function looksDiffable(since) {
68599
69483
  const s = since.trim();
68600
69484
  if (s.length === 0) return false;
68601
69485
  if (s.includes("..")) return true;
68602
- return /^[A-Za-z0-9._/-]+$/.test(s);
69486
+ if (/^\d+[smhd]$/i.test(s)) return false;
69487
+ return /^[A-Za-z0-9._/~^-]+$/.test(s);
68603
69488
  }
68604
69489
  async function investigate(input, deps) {
68605
69490
  const { code, db } = deps;
@@ -68622,7 +69507,8 @@ async function investigate(input, deps) {
68622
69507
  return ev;
68623
69508
  }
68624
69509
  const rawSeeds = await code.searchSymbols(hint, 5);
68625
- const ranked = rankSeeds(rawSeeds);
69510
+ const hintTokens = [...new Set(tokenize(hint))];
69511
+ const ranked = rankSeeds(rawSeeds, hintTokens);
68626
69512
  const seeds = ranked.map((r) => r.symbol);
68627
69513
  const top = seeds[0];
68628
69514
  if (!top) {
@@ -68674,6 +69560,13 @@ async function investigate(input, deps) {
68674
69560
  changes = null;
68675
69561
  }
68676
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
+ }
68677
69570
  const seedLine = top.startLine ?? 0;
68678
69571
  const seedEv = mkEv(
68679
69572
  "symbol",
@@ -68731,8 +69624,20 @@ async function investigate(input, deps) {
68731
69624
  );
68732
69625
  changeEvId = ev.id;
68733
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
+ }
68734
69637
  let analysis = null;
68735
69638
  const logEvIds = [];
69639
+ const directLogEvIds = [];
69640
+ const ambientLogEvIds = [];
68736
69641
  let logsCollected = false;
68737
69642
  let logsCompatibilityError;
68738
69643
  if (deps.logs) {
@@ -68749,8 +69654,19 @@ async function investigate(input, deps) {
68749
69654
  const from = logWindowFrom(input.since);
68750
69655
  analysis = await deps.logs.analyzeErrors({ service: input.service, from });
68751
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
+ ];
68752
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
+ );
68753
69668
  const tags = [];
69669
+ if (relevanceClass === "ambient") tags.push("ambient");
68754
69670
  if (s.isNew) tags.push("NEW");
68755
69671
  else if (s.ratio !== void 0 && Number.isFinite(s.ratio) && s.ratio >= 1.5) {
68756
69672
  tags.push(`spike x${s.ratio.toFixed(1)}`);
@@ -68771,15 +69687,21 @@ async function investigate(input, deps) {
68771
69687
  services: s.services,
68772
69688
  isNew: s.isNew ?? false,
68773
69689
  ratio: s.ratio ?? null,
68774
- sampleMessage: s.sampleMessage ?? null
69690
+ sampleMessage: s.sampleMessage ?? null,
69691
+ relevanceClass,
69692
+ relevanceReason
68775
69693
  },
68776
69694
  {},
68777
69695
  s.lastSeen || void 0,
68778
- 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
68779
69699
  );
68780
69700
  if (s.isNew) ev.isNew = s.isNew;
68781
69701
  if (typeof s.ratio === "number" && Number.isFinite(s.ratio)) ev.ratio = s.ratio;
68782
69702
  logEvIds.push(ev.id);
69703
+ if (relevanceClass === "direct") directLogEvIds.push(ev.id);
69704
+ else ambientLogEvIds.push(ev.id);
68783
69705
  }
68784
69706
  }
68785
69707
  } catch {
@@ -68792,11 +69714,8 @@ async function investigate(input, deps) {
68792
69714
  if (deps.mongo) {
68793
69715
  try {
68794
69716
  stateAnalysis = await deps.mongo.analyzeState();
68795
- const seedBase = top.filePath.split("/").pop() ?? "";
68796
- const terms = [
68797
- .../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(seedBase)])
68798
- ];
68799
- for (const s of selectStateSignals(stateAnalysis, terms)) {
69717
+ const stateTerms = [...new Set(tokenize(hint))];
69718
+ for (const s of selectStateSignals(stateAnalysis, stateTerms)) {
68800
69719
  const ev = mkEv("state", s.title, s.payload, {}, s.timestamp, s.relevance);
68801
69720
  stateEvIds.push(ev.id);
68802
69721
  stateCollections.add(s.collection);
@@ -68834,12 +69753,13 @@ async function investigate(input, deps) {
68834
69753
  queueRuntimeState = null;
68835
69754
  }
68836
69755
  }
68837
- const METRICS_TIMEOUT_MS = 1e4;
69756
+ const METRICS_TIMEOUT_MS = 3e4;
68838
69757
  const metricEvIds = [];
68839
69758
  const latencyMetricEvIds = [];
68840
69759
  const queueMetricEvIds = [];
68841
69760
  const queueMetricEvIdsByQueue = /* @__PURE__ */ new Map();
68842
69761
  let metricsCollected = false;
69762
+ let metricsFailureReason;
68843
69763
  if (deps.metrics) {
68844
69764
  const ac = new AbortController();
68845
69765
  let metricsTimerId;
@@ -68900,7 +69820,8 @@ async function investigate(input, deps) {
68900
69820
  }
68901
69821
  }
68902
69822
  metricsCollected = true;
68903
- } catch {
69823
+ } catch (metricsErr) {
69824
+ metricsFailureReason = metricsErr?.message?.slice(0, 120) ?? "unknown error";
68904
69825
  } finally {
68905
69826
  if (metricsTimerId !== void 0) clearTimeout(metricsTimerId);
68906
69827
  }
@@ -68928,9 +69849,12 @@ async function investigate(input, deps) {
68928
69849
  latencyMetricEvIds,
68929
69850
  queueBacklogEvIdsByQueue,
68930
69851
  queueStarvationEvIdsByQueue,
68931
- queueMetricEvIdsByQueue
69852
+ queueMetricEvIdsByQueue,
69853
+ sinceProvided: input.since !== void 0,
69854
+ graph
68932
69855
  });
68933
69856
  const validated = validateHypotheses(hyps, evidence2);
69857
+ const causeChains = buildCauseChains(validated, evidence2, graph, label);
68934
69858
  const findings2 = [];
68935
69859
  findings2.push({
68936
69860
  kind: "observation",
@@ -68985,24 +69909,43 @@ async function investigate(input, deps) {
68985
69909
  confidence: clamp015(0.4 + Math.min(m, 20) / 40),
68986
69910
  evidenceIds: [changeEvId]
68987
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
+ });
68988
69919
  }
68989
69920
  if (analysis !== null && analysis.signatures.length > 0) {
68990
69921
  const newN = analysis.newSignatures.length;
68991
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)` : "";
68992
69926
  findings2.push({
68993
69927
  kind: "observation",
68994
- title: `${analysis.signatures.length} error signature(s) (${newN} new, ${analysis.totalErrors} error(s)) \u2014 affected: ${affected}`,
68995
- 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,
68996
69930
  evidenceIds: logEvIds
68997
69931
  });
68998
- const top2 = analysis.signatures[0];
68999
- if (top2 !== void 0) {
69000
- 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)})` : "";
69001
69936
  findings2.push({
69002
69937
  kind: "anomaly",
69003
- 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)}`,
69004
69939
  confidence: 0.7,
69005
- 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)
69006
69949
  });
69007
69950
  }
69008
69951
  }
@@ -69100,13 +70043,17 @@ async function investigate(input, deps) {
69100
70043
  metadata: { blastRadius }
69101
70044
  });
69102
70045
  }
69103
- 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
+ );
69104
70051
  causeInputs.push({
69105
70052
  id: "cause:deployment-regression",
69106
70053
  title: `Recent change to ${top.name} in ${input.since}..HEAD may have introduced the regression`,
69107
70054
  category: "deployment-regression",
69108
70055
  sourceEvidenceIds: [changeEvId, seedEv.id],
69109
- baseScore: clamp015(0.25 + (queueHits.length > 0 ? 0.05 : 0)),
70056
+ baseScore: clamp015((seedInChanges ? 0.45 : 0.25) + (queueHits.length > 0 ? 0.05 : 0)),
69110
70057
  metadata: { blastRadius }
69111
70058
  });
69112
70059
  }
@@ -69120,7 +70067,7 @@ async function investigate(input, deps) {
69120
70067
  metadata: { blastRadius }
69121
70068
  });
69122
70069
  }
69123
- if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0) {
70070
+ if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0 && directLogEvIds.length > 0) {
69124
70071
  const firstQueue = queueHits[0];
69125
70072
  const queueLabel = firstQueue !== void 0 ? `"${firstQueue.queueName}" (${firstQueue.producerSymbol ?? "unknown"} -> ${firstQueue.workerSymbol ?? "unknown"})` : "the queue path";
69126
70073
  const topSig = analysis.signatures[0];
@@ -69128,7 +70075,7 @@ async function investigate(input, deps) {
69128
70075
  id: "cause:error-correlation",
69129
70076
  title: `Runtime errors (${analysis.totalErrors}${topSig ? `, top ${topSig.key}` : ""}) correlate with the implicated queue path ${queueLabel}`,
69130
70077
  category: "error-correlation",
69131
- sourceEvidenceIds: logEvIds.slice(0, 3),
70078
+ sourceEvidenceIds: directLogEvIds.slice(0, 3),
69132
70079
  baseScore: 0.3,
69133
70080
  metadata: { blastRadius }
69134
70081
  });
@@ -69167,19 +70114,15 @@ async function investigate(input, deps) {
69167
70114
  providerReliability,
69168
70115
  request: { hint: input.hint, service: input.service }
69169
70116
  });
69170
- const evidenceConfidence = computeWeightedEvidenceConfidence(evidence2);
70117
+ const evidenceConfidence = computeWeightedEvidenceConfidence(
70118
+ evidence2,
70119
+ ambientLogEvIds.length > 0 ? new Set(ambientLogEvIds) : void 0
70120
+ );
69171
70121
  const seedResolved = seeds.length > 0 ? 1 : 0;
69172
70122
  const confidence = clamp015(0.5 * evidenceConfidence + 0.5 * seedResolved);
69173
70123
  const area = ctx.community?.name ?? top.filePath;
69174
70124
  const topCause = rankedCauses[0];
69175
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.`;
69176
- let recentChanges;
69177
- if (deps.repoPath && input.since) {
69178
- try {
69179
- recentChanges = await collectGitChanges({ repoPath: deps.repoPath, since: input.since });
69180
- } catch {
69181
- }
69182
- }
69183
70126
  const nextActions = buildNextActions(top, ctx, impact, queueHits, changes, input);
69184
70127
  if (ownershipEstimate?.likelyMaintainer) {
69185
70128
  nextActions.unshift(
@@ -69203,15 +70146,18 @@ async function investigate(input, deps) {
69203
70146
  confidence,
69204
70147
  nextActions,
69205
70148
  ownership: ownershipEstimate,
70149
+ causeChains: causeChains.length > 0 ? causeChains : void 0,
69206
70150
  ...recentChanges !== void 0 ? { recentChanges } : {}
69207
70151
  };
69208
- 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 } : {
69209
70153
  elasticsearch: deps.logs != null,
69210
70154
  mongodb: deps.mongo != null,
69211
70155
  grafana: deps.metrics != null,
69212
70156
  metricsCollected,
70157
+ metricsFailureReason,
69213
70158
  logsCollected,
69214
- logsCompatibilityError
70159
+ logsCompatibilityError,
70160
+ sinceProvided: input.since !== void 0
69215
70161
  };
69216
70162
  const gapAnalysis = detectMissingEvidence(report, connectorFlags);
69217
70163
  report.gapAnalysis = gapAnalysis;
@@ -69355,11 +70301,11 @@ function groupQueueEvidence(evidence2) {
69355
70301
  return map2;
69356
70302
  }
69357
70303
  function queueEvidenceIds(evidence2) {
69358
- const ids = /* @__PURE__ */ new Set();
70304
+ const ids2 = /* @__PURE__ */ new Set();
69359
70305
  for (const e of evidence2) {
69360
- 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);
69361
70307
  }
69362
- return ids;
70308
+ return ids2;
69363
70309
  }
69364
70310
  var CONFIDENCE_EXPLAIN_THRESHOLD = 0.8;
69365
70311
  function explainLowConfidence(r) {
@@ -69522,6 +70468,12 @@ function renderReport2(r) {
69522
70468
  }
69523
70469
  lines.push("");
69524
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
+ }
69525
70477
  if (r.hypotheses.length === 0) {
69526
70478
  lines.push("(none)");
69527
70479
  } else {
@@ -69536,6 +70488,19 @@ function renderReport2(r) {
69536
70488
  }
69537
70489
  }
69538
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
+ }
69539
70504
  lines.push("## Evidence gaps (what we don't know)");
69540
70505
  if (r.gapAnalysis.gaps.length === 0) {
69541
70506
  lines.push("(no major evidence gaps)");
@@ -69666,6 +70631,12 @@ function reportToMarkdown(r) {
69666
70631
  }
69667
70632
  out.push("");
69668
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
+ }
69669
70640
  if (r.hypotheses.length === 0) {
69670
70641
  out.push("_none_");
69671
70642
  } else {
@@ -69881,7 +70852,8 @@ async function reconstructChangeTimeline(input, deps) {
69881
70852
  }
69882
70853
  }
69883
70854
  }
69884
- 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 + ".";
69885
70857
  const note = "Changes are evidence, not conclusions \u2014 a change in this window is not automatically the cause.";
69886
70858
  return {
69887
70859
  window: {
@@ -69904,6 +70876,9 @@ function renderChangeTimeline(t) {
69904
70876
  lines.push("# Change Timeline");
69905
70877
  lines.push("");
69906
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);
69907
70882
  lines.push(t.summary);
69908
70883
  lines.push("");
69909
70884
  lines.push("> " + t.note);
@@ -69926,9 +70901,13 @@ function renderChangeTimeline(t) {
69926
70901
  if (t.changeImpact !== null) {
69927
70902
  lines.push("");
69928
70903
  lines.push("## Change impact");
69929
- lines.push(
69930
- t.changeImpact.summary + " Affected flows: " + t.changeImpact.affectedFlows.length + "."
69931
- );
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
+ }
69932
70911
  }
69933
70912
  return lines.join("\n");
69934
70913
  }
@@ -69988,6 +70967,9 @@ function renderWhatChanged(r) {
69988
70967
  const lines = [];
69989
70968
  lines.push("# What changed");
69990
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);
69991
70973
  lines.push(r.summary);
69992
70974
  lines.push("");
69993
70975
  lines.push("> " + r.note);
@@ -70017,6 +70999,11 @@ function renderWhatChanged(r) {
70017
70999
  lines.push(
70018
71000
  "## Affected flows: " + r.changeImpact.affectedFlows.length + " execution flow(s) affected"
70019
71001
  );
71002
+ if (r.changeImpact.affectedFlows.length > 0) {
71003
+ for (const f of r.changeImpact.affectedFlows) {
71004
+ lines.push(" - " + f.flowName);
71005
+ }
71006
+ }
70020
71007
  }
70021
71008
  return lines.join("\n");
70022
71009
  }
@@ -70147,7 +71134,7 @@ async function discoverArchitecture(deps) {
70147
71134
  })();
70148
71135
  const largestSubsystem = subsystems[0];
70149
71136
  const largestDesc = largestSubsystem != null ? `${largestSubsystem.name} with ${largestSubsystem.members} symbols` : "none";
70150
- 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.`;
70151
71138
  return {
70152
71139
  nodeStats,
70153
71140
  subsystems,
@@ -70215,7 +71202,7 @@ function renderArchitecture(m) {
70215
71202
  }
70216
71203
  lines.push("");
70217
71204
  lines.push("## Fragility");
70218
- lines.push(`- Dead-code symbols: ${m.fragile.deadCode}`);
71205
+ lines.push(`- Unreferenced symbols: ${m.fragile.deadCode}`);
70219
71206
  lines.push(`- High-coupling pairs (co-changes \u2265 3): ${m.fragile.highCouplingPairs}`);
70220
71207
  return lines.join("\n");
70221
71208
  }
@@ -70406,6 +71393,13 @@ function renderOwnership(o) {
70406
71393
  lines.push("");
70407
71394
  if (o.file === null) {
70408
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
+ }
70409
71403
  return lines.join("\n");
70410
71404
  }
70411
71405
  if (o.symbol !== null) {
@@ -70531,17 +71525,35 @@ function generatePostmortem(r) {
70531
71525
  );
70532
71526
  }
70533
71527
  } else {
70534
- if (supportedHypotheses.length > 0) {
70535
- 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:");
70536
71530
  lines.push("");
70537
- for (const h of supportedHypotheses) {
70538
- lines.push(
70539
- `- **${h.category}:** ${h.statement} _(confidence ${h.confidence.toFixed(2)})_`
70540
- );
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
+ }
70541
71553
  }
70542
71554
  }
70543
71555
  if (commitEvidence.length > 0) {
70544
- if (supportedHypotheses.length > 0) lines.push("");
71556
+ if (supportedHypotheses.length > 0 || (r.causeChains?.length ?? 0) > 0) lines.push("");
70545
71557
  lines.push("Recent changes (commit evidence) present during this incident:");
70546
71558
  lines.push("");
70547
71559
  for (const e of commitEvidence) {
@@ -70602,19 +71614,29 @@ function generatePostmortem(r) {
70602
71614
  lines.push("");
70603
71615
  lines.push("## Follow-up actions");
70604
71616
  lines.push("");
70605
- const seen = /* @__PURE__ */ new Set();
70606
71617
  const checkboxItems = [];
71618
+ const seen = /* @__PURE__ */ new Set();
70607
71619
  for (const action of r.nextActions) {
70608
71620
  if (!seen.has(action)) {
70609
71621
  seen.add(action);
70610
71622
  checkboxItems.push(action);
70611
71623
  }
70612
71624
  }
71625
+ const seenGapSources = /* @__PURE__ */ new Set();
70613
71626
  for (const gap of r.gapAnalysis.gaps) {
70614
- const wireAction = `Wire ${gap.nextSource} to close the '${gap.dimension}' evidence gap`;
70615
- if (!seen.has(wireAction)) {
70616
- seen.add(wireAction);
70617
- 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);
70618
71640
  }
70619
71641
  }
70620
71642
  if (checkboxItems.length === 0) {
@@ -70746,11 +71768,113 @@ var TOPIC_MAP = {
70746
71768
  }
70747
71769
  };
70748
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
+ }
70749
71812
  function detectMode(d) {
70750
71813
  if (/\b(ignore|exclude|without|skip|drop)\b/.test(d)) return "ignore";
70751
71814
  if (/\b(focus|only|just|concentrate|look at)\b/.test(d)) return "focus";
70752
71815
  return "none";
70753
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
+ }
70754
71878
  function matchedTopics(d) {
70755
71879
  return ALL_TOPICS.filter((t) => {
70756
71880
  const entry2 = TOPIC_MAP[t];
@@ -70791,11 +71915,54 @@ function topicKeywords(topics) {
70791
71915
  }
70792
71916
  return kws;
70793
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
+ }
70794
71952
  function refineInvestigation(r, directive) {
70795
71953
  const d = directive.toLowerCase();
71954
+ if (hasFocusVerb(d) && hasIgnoreVerb(d)) {
71955
+ return applyMixedDirective(r, directive, d);
71956
+ }
70796
71957
  const mode = detectMode(d);
70797
71958
  const topics = matchedTopics(d);
70798
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
+ }
70799
71966
  const recognizedList = ALL_TOPICS.join(", ");
70800
71967
  const note2 = "No specific topic directive recognized. Recognized topics: " + recognizedList + '. Example usage: "focus on queue behavior", "ignore deployment changes".';
70801
71968
  return {
@@ -70843,7 +72010,7 @@ function renderRefined(r, v) {
70843
72010
  const lines = [];
70844
72011
  lines.push("# Refined investigation \u2014 " + r.input.hint);
70845
72012
  lines.push("");
70846
- 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)";
70847
72014
  lines.push(" [mode: " + modeLabel + "] reusing saved evidence, no re-query");
70848
72015
  lines.push("");
70849
72016
  lines.push("## Hypotheses");
@@ -70898,20 +72065,175 @@ function refinedToJSON(r, v) {
70898
72065
 
70899
72066
  // ../../packages/engine/src/onboard.ts
70900
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
+ }
70901
72181
  async function buildOnboarding(input, deps) {
70902
72182
  const architecture = await discoverArchitecture({ code: deps.code, db: deps.db });
70903
- const invs = await listInvestigations(deps.db, 8);
70904
- const pastIncidents = invs.map((i) => ({
70905
- id: i.id,
70906
- title: i.title,
70907
- createdAt: i.createdAt != null ? new Date(i.createdAt).toISOString() : null
70908
- }));
70909
- const ownership = input.area != null ? await estimateOwnership(input.area, { code: deps.code, repoPath: deps.repoPath }) : null;
70910
- const largestName = architecture.subsystems[0]?.name ?? "n/a";
70911
- 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 + '".' : "");
70912
72234
  return {
70913
- area: input.area ?? null,
70914
- architecture,
72235
+ area: area ?? null,
72236
+ architecture: filteredArchitecture,
70915
72237
  ownership,
70916
72238
  pastIncidents,
70917
72239
  summary
@@ -70970,7 +72292,7 @@ function renderOnboarding(g) {
70970
72292
  lines.push("");
70971
72293
  lines.push("## What usually breaks");
70972
72294
  lines.push("");
70973
- lines.push(`- Dead-code symbols: ${g.architecture.fragile.deadCode}`);
72295
+ lines.push(`- Unreferenced symbols: ${g.architecture.fragile.deadCode}`);
70974
72296
  lines.push(
70975
72297
  `- High-coupling pairs (co-changes \u2265 3): ${g.architecture.fragile.highCouplingPairs}`
70976
72298
  );
@@ -71064,6 +72386,7 @@ var SCENARIOS = [
71064
72386
  since: "HEAD~10",
71065
72387
  expectedSignals: [
71066
72388
  { key: "seed", label: "Seed symbols resolved" },
72389
+ { key: "commit", label: "Recent change evidence found" },
71067
72390
  { key: "hyp:deployment-regression", label: "deployment-regression hypothesis present" },
71068
72391
  { key: "actions", label: "Next actions generated" }
71069
72392
  ],
@@ -71119,6 +72442,8 @@ function evaluateScenario(scenario, report) {
71119
72442
  ok = report.gapAnalysis.gaps.length > 0;
71120
72443
  } else if (signal.key === "actions") {
71121
72444
  ok = report.nextActions.length > 0;
72445
+ } else if (signal.key === "commit") {
72446
+ ok = report.evidence.some((e) => e.kind === "commit");
71122
72447
  } else if (signal.key.startsWith("hyp:")) {
71123
72448
  const category = signal.key.slice(4);
71124
72449
  ok = report.hypotheses.some((h) => h.category === category);
@@ -71159,6 +72484,16 @@ function renderSimulation(scenario, report, evaluation) {
71159
72484
  "Form your own hypothesis before reading on \u2014 then compare it with what Horus found."
71160
72485
  );
71161
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
+ }
71162
72497
  lines.push("## Horus investigation");
71163
72498
  lines.push("");
71164
72499
  lines.push(report.summary);
@@ -71190,6 +72525,28 @@ function renderSimulation(scenario, report, evaluation) {
71190
72525
  for (const tip of scenario.coachingTips) {
71191
72526
  lines.push(`- ${tip}`);
71192
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
+ }
71193
72550
  return lines.join("\n");
71194
72551
  }
71195
72552
 
@@ -71361,6 +72718,54 @@ function validateNarrative(output, input) {
71361
72718
  }
71362
72719
  }
71363
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
+ }
71364
72769
  return { valid: errors2.length === 0, errors: errors2 };
71365
72770
  }
71366
72771
  async function renderNarrative(input, opts = {}) {
@@ -71378,11 +72783,11 @@ async function renderNarrative(input, opts = {}) {
71378
72783
  fromProvider: false,
71379
72784
  validationErrors: validation.errors
71380
72785
  };
71381
- } catch {
72786
+ } catch (err) {
71382
72787
  return {
71383
72788
  output: deterministicFallback(input),
71384
72789
  fromProvider: false,
71385
- validationErrors: ["Provider threw an error"]
72790
+ validationErrors: [err instanceof Error ? err.message : "Provider threw an error"]
71386
72791
  };
71387
72792
  }
71388
72793
  }
@@ -71449,7 +72854,26 @@ function buildPrompt(input, ceiling) {
71449
72854
  (e) => `- [${e.id}] (${e.kind}) ${e.title}${e.excerpt ? `: ${e.excerpt}` : ""}`
71450
72855
  ).join("\n");
71451
72856
  const causeLines = input.suspectedCauses.map((c) => `- ${c.label} (score: ${c.score})`).join("\n");
71452
- 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.
71453
72877
 
71454
72878
  Investigation hint: ${input.hint}
71455
72879
  Deterministic summary: ${input.deterministicSummary}
@@ -71459,23 +72883,30 @@ ${evidenceLines}
71459
72883
 
71460
72884
  Suspected causes:
71461
72885
  ${causeLines}
72886
+ ${hypothesisLines ? `
72887
+ Deterministic hypotheses (provide a second-pass judgment for each):
72888
+ ${hypothesisLines}` : ""}
71462
72889
 
71463
72890
  Known services: ${input.knownServices.join(", ")}
71464
72891
 
71465
72892
  Return ONLY valid JSON with this exact shape:
71466
72893
  {
71467
72894
  "what": "<what happened \u2014 one concise paragraph>",
71468
- "why": "<root cause analysis grounded in the evidence above>",
72895
+ "why": "<root cause narrative grounded in the evidence above>",
71469
72896
  "whereNext": ["<action 1>", "<action 2>"],
71470
72897
  "citations": [{"evidenceId": "<id from the evidence list>", "rationale": "<why this supports the claim>"}],
71471
72898
  "confidence": <number between 0 and ${ceiling}>,
71472
- "mentionedServices": ["<service name from known list only>"]
72899
+ "mentionedServices": ["<service name from known list only>"],
72900
+ ${hypothesisJudgmentSchema}
71473
72901
  }
71474
72902
 
71475
72903
  Hard rules:
71476
72904
  - confidence must not exceed ${ceiling}
71477
72905
  - only cite evidence IDs from the list above \u2014 any other ID is a hallucination
71478
- - 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`;
71479
72910
  }
71480
72911
  function parseOutput(raw, input, ceiling) {
71481
72912
  let parsed;
@@ -71489,21 +72920,52 @@ function parseOutput(raw, input, ceiling) {
71489
72920
  parsed = JSON.parse(jsonMatch[0]);
71490
72921
  }
71491
72922
  const confidence = Math.min(
71492
- typeof parsed.confidence === "number" ? parsed.confidence : input.reportConfidence,
72923
+ typeof parsed["confidence"] === "number" ? parsed["confidence"] : input.reportConfidence,
71493
72924
  ceiling
71494
72925
  );
72926
+ const citations = Array.isArray(parsed["citations"]) ? parsed["citations"] : [];
71495
72927
  const output = {
71496
- what: typeof parsed.what === "string" ? parsed.what : "",
71497
- why: typeof parsed.why === "string" ? parsed.why : "",
71498
- whereNext: Array.isArray(parsed.whereNext) ? parsed.whereNext.map(String) : [],
71499
- 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,
71500
72932
  confidence
71501
72933
  };
71502
- if (Array.isArray(parsed.mentionedServices)) {
71503
- 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
+ };
71504
72960
  }
71505
72961
  return output;
71506
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
+ }
71507
72969
 
71508
72970
  // ../../packages/ai/src/fake-provider.ts
71509
72971
  init_cjs_shims();
@@ -71543,6 +73005,12 @@ init_cjs_shims();
71543
73005
  init_cjs_shims();
71544
73006
 
71545
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
+ }
71546
73014
  function buildNarrativeInput(report) {
71547
73015
  return {
71548
73016
  investigationId: report.id,
@@ -71551,7 +73019,8 @@ function buildNarrativeInput(report) {
71551
73019
  evidence: report.evidence.map((e) => ({
71552
73020
  id: e.id,
71553
73021
  kind: e.kind,
71554
- title: e.title
73022
+ title: e.title,
73023
+ excerpt: extractEvidenceExcerpt(e)
71555
73024
  })),
71556
73025
  knownServices: report.input.service ? [report.input.service] : [],
71557
73026
  suspectedCauses: report.suspectedCauses.map((c) => ({
@@ -71563,9 +73032,83 @@ function buildNarrativeInput(report) {
71563
73032
  findings: report.findings.map((f) => ({
71564
73033
  title: f.title,
71565
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
71566
73043
  }))
71567
73044
  };
71568
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
+ }
71569
73112
  async function runInvestigate(hint, opts) {
71570
73113
  try {
71571
73114
  const config = await loadConfig(opts.config, { name: opts.name });
@@ -71625,33 +73168,22 @@ async function runInvestigate(hint, opts) {
71625
73168
  const rendered = format === "json" ? reportToJSON(report) : format === "markdown" || format === "md" ? reportToMarkdown(report) : renderReport2(report);
71626
73169
  console.log(rendered);
71627
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}`));
71628
73174
  const narrativeInput = buildNarrativeInput(report);
71629
- const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
71630
73175
  const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
71631
73176
  if (!fromProvider) {
71632
- console.error(import_picocolors5.default.yellow(`[ai] Provider unavailable \u2014 deterministic output shown above.`));
71633
- if (validationErrors?.length) {
71634
- console.error(import_picocolors5.default.dim(` ${validationErrors[0]}`));
71635
- }
73177
+ const reason = classifyAIFailure(validationErrors?.[0]);
73178
+ console.error(import_picocolors5.default.yellow(`[ai] fallback to deterministic \u2014 ${reason}`));
71636
73179
  } else {
71637
- const sep = "\u2500".repeat(60);
71638
- console.log(`
71639
- ${sep}`);
71640
- console.log(import_picocolors5.default.bold("AI Narrative"));
71641
- console.log(sep);
71642
- console.log(import_picocolors5.default.bold("What:"), output.what);
71643
- console.log(import_picocolors5.default.bold("Why:"), output.why);
71644
- if (output.whereNext.length > 0) {
71645
- console.log(import_picocolors5.default.bold("Next steps:"));
71646
- for (const step of output.whereNext) {
71647
- console.log(` \u2022 ${step}`);
71648
- }
71649
- }
71650
- if (output.citations.length > 0) {
71651
- console.log(import_picocolors5.default.dim(`
71652
- 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 {
71653
73185
  }
71654
- console.log(import_picocolors5.default.dim(`AI confidence: ${(output.confidence * 100).toFixed(0)}%`));
73186
+ renderStoredAIJudgment(stored);
71655
73187
  }
71656
73188
  }
71657
73189
  } finally {
@@ -71787,9 +73319,15 @@ async function runBlastRadius(query, opts) {
71787
73319
  try {
71788
73320
  const r = await analyzeBlastRadius(query, { code, db }, opts.depth ?? 3);
71789
73321
  if (!r) {
71790
- 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"`));
71791
73324
  return 1;
71792
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
+ }
71793
73331
  console.log(opts.json ? blastRadiusToJSON(r) : renderBlastRadius(r));
71794
73332
  } finally {
71795
73333
  await sql2.end();
@@ -71862,6 +73400,26 @@ async function runSearch(query, opts) {
71862
73400
 
71863
73401
  // ../../packages/cli/src/commands/investigations.ts
71864
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
71865
73423
  async function runInvestigations(opts) {
71866
73424
  const config = await loadConfig(opts.config);
71867
73425
  const { db, sql: sql2 } = createDb(config.database.url);
@@ -71871,7 +73429,7 @@ async function runInvestigations(opts) {
71871
73429
  console.log('No investigations yet. Run: horus investigate "<hint>"');
71872
73430
  } else {
71873
73431
  for (const row of rows) {
71874
- const ts = row.createdAt.toISOString();
73432
+ const ts = formatDateTime(row.createdAt);
71875
73433
  const title = (row.title ?? "").length > 60 ? (row.title ?? "").slice(0, 57) + "..." : row.title ?? "";
71876
73434
  console.log(`${row.id} ${ts} ${title}`);
71877
73435
  }
@@ -71906,6 +73464,30 @@ async function runReplay(id, opts) {
71906
73464
  const fmt = opts.format ?? "text";
71907
73465
  const out = fmt === "json" ? reportToJSON(report) : fmt === "markdown" || fmt === "md" ? reportToMarkdown(report) : renderReport2(report);
71908
73466
  console.log(out);
73467
+ if (opts.ai && fmt !== "json") {
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."));
73471
+ } else {
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]}`));
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);
73488
+ }
73489
+ }
73490
+ }
71909
73491
  } finally {
71910
73492
  await sql2.end();
71911
73493
  }
@@ -71963,7 +73545,79 @@ async function runPostmortem(id, opts) {
71963
73545
  }
71964
73546
  report = migrateReport(row.report);
71965
73547
  }
71966
- const content = generatePostmortem(report);
73548
+ let content = generatePostmortem(report);
73549
+ if (opts.aiSummary) {
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})_
73554
+
73555
+ `;
73556
+ content += `**What happened:** ${storedJudgment.what}
73557
+
73558
+ `;
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})_
73566
+ `;
73567
+ }
73568
+ if (storedJudgment.whereNext.length > 0) {
73569
+ content += "\n**Next steps:**\n";
73570
+ for (const step of storedJudgment.whereNext) {
73571
+ content += `- ${step}
73572
+ `;
73573
+ }
73574
+ }
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) {
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 += `
73614
+ **Cited evidence:** ${output.citations.map((c) => c.evidenceId).join(", ")}
73615
+ `;
73616
+ }
73617
+ report.aiJudgment = narrativeOutputToStoredJudgment(output, "anthropic");
73618
+ }
73619
+ }
73620
+ }
71967
73621
  if (opts.output) {
71968
73622
  const outputPath = (0, import_node_path7.resolve)(opts.output);
71969
73623
  if ((0, import_node_fs6.existsSync)(outputPath) && !opts.force) {
@@ -72033,7 +73687,7 @@ async function runScores(opts) {
72033
73687
  }
72034
73688
  for (const s of scored) {
72035
73689
  console.log(
72036
- " " + 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 ?? "")
72037
73691
  );
72038
73692
  }
72039
73693
  const avg = Math.round(
@@ -72065,6 +73719,18 @@ async function runAsk(id, directive, opts) {
72065
73719
  const report = migrateReport(row.report);
72066
73720
  const v = refineInvestigation(report, directive);
72067
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
+ }
72068
73734
  } catch (err) {
72069
73735
  const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
72070
73736
  if (code === "22P02") {
@@ -72209,6 +73875,7 @@ async function runLogs(service, opts) {
72209
73875
  return 1;
72210
73876
  }
72211
73877
  const resolvedService = service ?? renv.connectors.elasticsearch?.serviceName;
73878
+ const indexPattern = renv.connectors.elasticsearch?.indexPattern ?? "*";
72212
73879
  try {
72213
73880
  const compat = await logs.checkCompatibility({
72214
73881
  requiresService: resolvedService !== void 0,
@@ -72234,7 +73901,8 @@ async function runLogs(service, opts) {
72234
73901
  } catch {
72235
73902
  console.warn(import_picocolors20.default.dim("[warn] Elasticsearch compatibility check unavailable \u2014 proceeding."));
72236
73903
  }
72237
- 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", " ");
72238
73906
  if (opts.raw === true) {
72239
73907
  const records = await logs.searchLogs({
72240
73908
  service: resolvedService,
@@ -72256,22 +73924,42 @@ async function runLogs(service, opts) {
72256
73924
  }
72257
73925
  return 0;
72258
73926
  }
72259
- const analysis = await logs.analyzeErrors({
73927
+ let analysis = await logs.analyzeErrors({
72260
73928
  service: resolvedService,
72261
73929
  from,
72262
73930
  text: opts.grep
72263
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
+ }
72264
73942
  console.log(
72265
73943
  import_picocolors20.default.bold(`Error analysis`) + import_picocolors20.default.dim(
72266
- ` \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}"` : "")
72267
73945
  )
72268
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
+ }
72269
73951
  console.log(
72270
73952
  ` ${analysis.totalErrors} error(s) \xB7 ${analysis.signatures.length} signature(s) \xB7 ${import_picocolors20.default.yellow(String(analysis.newSignatures.length))} new`
72271
73953
  );
72272
73954
  console.log("");
72273
73955
  if (analysis.signatures.length === 0) {
72274
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).`));
72275
73963
  return 0;
72276
73964
  }
72277
73965
  for (const s of analysis.signatures) {
@@ -72396,18 +74084,29 @@ async function runMetrics(hint, opts) {
72396
74084
  const flagged = findings2.filter((f) => f.anomaly !== "none");
72397
74085
  const ok = findings2.filter((f) => f.anomaly === "none");
72398
74086
  if (findings2.length === 0) {
72399
- console.log(
72400
- import_picocolors21.default.dim(
72401
- hint !== void 0 ? `No panels matched hint "${hint}".` : "No panels found in configured Grafana dashboards."
72402
- )
72403
- );
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
+ }
72404
74101
  return 0;
72405
74102
  }
72406
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(", ")}]`) : "";
72407
74106
  console.log(
72408
74107
  import_picocolors21.default.bold(
72409
74108
  `Grafana metrics${hintSuffix} \u2014 ${findings2.length} series across panels`
72410
- )
74109
+ ) + matchSuffix
72411
74110
  );
72412
74111
  console.log(
72413
74112
  import_picocolors21.default.dim(
@@ -72474,8 +74173,9 @@ async function runState(opts) {
72474
74173
  const analysis = await mongo.analyzeState(
72475
74174
  staleHours !== void 0 ? { staleHours } : {}
72476
74175
  );
74176
+ const discoveryNote = analysis.autoDiscovered ? import_picocolors22.default.dim(` (${analysis.collections.length} collections, auto-discovered)`) : "";
72477
74177
  console.log(
72478
- 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
72479
74179
  );
72480
74180
  console.log("");
72481
74181
  let signals = 0;
@@ -72493,9 +74193,23 @@ async function runState(opts) {
72493
74193
  console.log(flags.length > 0 ? `${head} ${flags.join(" ")}` : import_picocolors22.default.dim(head));
72494
74194
  }
72495
74195
  console.log("");
72496
- console.log(
72497
- 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).`)
72498
- );
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
+ }
72499
74213
  return 0;
72500
74214
  } finally {
72501
74215
  await mongo.close();
@@ -72517,7 +74231,7 @@ async function runInit(opts) {
72517
74231
  const name = opts.name ?? (0, import_node_path8.basename)(root);
72518
74232
  const envName = opts.env ?? "production";
72519
74233
  const repo = { name, path: root };
72520
- if (opts.axon) repo["axon"] = { hostUrl: opts.axon };
74234
+ if (opts.source) repo["source"] = { hostUrl: opts.source };
72521
74235
  const file = {
72522
74236
  version: 1,
72523
74237
  project: {
@@ -72531,9 +74245,9 @@ async function runInit(opts) {
72531
74245
  console.log(`${import_picocolors23.default.green("\u2713")} Initialized Horus project ${import_picocolors23.default.bold(name)}`);
72532
74246
  console.log(import_picocolors23.default.dim(` config: ${configPath}`));
72533
74247
  console.log(import_picocolors23.default.dim(` registered: horus investigate --name ${name} "<hint>"`));
72534
- if (!opts.axon) {
74248
+ if (!opts.source) {
72535
74249
  console.log(
72536
- import_picocolors23.default.dim(" no source-intelligence host set \u2014 run `horus index` to analyze + host, or pass --axon <url>")
74250
+ import_picocolors23.default.dim(" no source-intelligence host set \u2014 run `horus index` to analyze + host, or pass --source <url>")
72537
74251
  );
72538
74252
  }
72539
74253
  console.log(
@@ -72588,8 +74302,7 @@ async function runSetup(opts) {
72588
74302
  write(
72589
74303
  import_picocolors25.default.dim(
72590
74304
  ` install it (Python 3.11+ required):
72591
- uv tool install axoniq==${PINNED_SOURCE_VERSION}
72592
- or: pip install axoniq==${PINNED_SOURCE_VERSION}
74305
+ pip install horus-source
72593
74306
  ensure ~/.local/bin is on your PATH`
72594
74307
  )
72595
74308
  );
@@ -72601,8 +74314,7 @@ async function runSetup(opts) {
72601
74314
  write(
72602
74315
  import_picocolors25.default.dim(
72603
74316
  ` update it:
72604
- uv tool install axoniq==${PINNED_SOURCE_VERSION}
72605
- or: pip install axoniq==${PINNED_SOURCE_VERSION}`
74317
+ pip install --upgrade horus-source`
72606
74318
  )
72607
74319
  );
72608
74320
  } else {
@@ -72706,12 +74418,192 @@ async function runSetup(opts) {
72706
74418
  init_cjs_shims();
72707
74419
  var import_node_readline = require("readline");
72708
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");
72709
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
72710
74602
  var SUPPORTED = ["elasticsearch", "mongodb", "grafana", "redis"];
72711
74603
  async function runConnect(type, opts) {
72712
74604
  if (!SUPPORTED.includes(type)) {
72713
74605
  console.error(
72714
- 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(`
72715
74607
  supported: ${SUPPORTED.join(", ")}`)
72716
74608
  );
72717
74609
  return 1;
@@ -72742,18 +74634,20 @@ async function runConnect(type, opts) {
72742
74634
  if (!probeResult.ok) {
72743
74635
  console.error(
72744
74636
  `
72745
- ${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.")
72746
74638
  );
72747
74639
  return 1;
72748
74640
  }
72749
- console.log(`
72750
- ${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
+ );
72751
74645
  }
72752
74646
  const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0;
72753
74647
  if (hasLiteralCredentials) {
72754
74648
  if (isGitTracked(configPath, root)) {
72755
74649
  console.error(
72756
- 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(
72757
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."
72758
74652
  )
72759
74653
  );
@@ -72762,29 +74656,52 @@ ${import_picocolors26.default.green("\u2713")} ${connectorType} reachable ${impo
72762
74656
  ensureCredentialGitignore(root);
72763
74657
  }
72764
74658
  patchLocalConnector(configPath, connectorType, patch, filled.env);
72765
- 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
+ );
72766
74662
  printSummary(connectorType, filled);
72767
- console.log(import_picocolors26.default.dim(`
74663
+ console.log(import_picocolors27.default.dim(`
72768
74664
  run: horus investigate "<hint>"`));
72769
74665
  return 0;
72770
74666
  } catch (err) {
72771
- 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));
72772
74672
  return 1;
72773
74673
  }
72774
74674
  }
72775
74675
  async function fillInteractive(type, opts) {
72776
74676
  const needsInteraction = missingRequired(type, opts);
72777
74677
  if (!needsInteraction) return opts;
72778
- console.log(`
72779
- ${import_picocolors26.default.bold(`Connect ${type}`)} ${import_picocolors26.default.dim("(press Enter to skip optional fields)")}
72780
- `);
74678
+ console.log(
74679
+ `
74680
+ ${import_picocolors27.default.bold(`Connect ${type}`)} ${import_picocolors27.default.dim("(press Enter to skip optional fields)")}
74681
+ `
74682
+ );
72781
74683
  const filled = { ...opts };
72782
74684
  switch (type) {
72783
74685
  case "elasticsearch":
72784
74686
  filled.url = filled.url ?? await ask("URL", "https://elastic.example.com");
72785
74687
  filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
72786
74688
  filled.password = filled.password ?? (await askPassword("Password") || void 0);
72787
- 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
+ }
72788
74705
  filled.service = filled.service ?? (await ask("Service name", "") || void 0);
72789
74706
  break;
72790
74707
  case "mongodb":
@@ -72796,18 +74713,45 @@ ${import_picocolors26.default.bold(`Connect ${type}`)} ${import_picocolors26.def
72796
74713
  filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
72797
74714
  filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
72798
74715
  filled.password = filled.password ?? (await askPassword("Password") || void 0);
72799
- 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
+ }
72800
74732
  break;
72801
- 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
+ );
72802
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
+ }
72803
74746
  break;
74747
+ }
72804
74748
  }
72805
74749
  return filled;
72806
74750
  }
72807
74751
  function missingRequired(type, opts) {
72808
74752
  switch (type) {
72809
74753
  case "elasticsearch":
72810
- return !opts.url || !opts.indexPattern;
74754
+ return !opts.url || !opts.indexPattern && !opts.indexPatterns?.length;
72811
74755
  case "mongodb":
72812
74756
  return !opts.url || !opts.database;
72813
74757
  case "grafana":
@@ -72818,10 +74762,14 @@ function missingRequired(type, opts) {
72818
74762
  }
72819
74763
  function ask(label, placeholder = "", required = true) {
72820
74764
  return new Promise((resolve8) => {
72821
- const hint = placeholder ? import_picocolors26.default.dim(` (${placeholder})`) : "";
72822
- 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]");
72823
74767
  process.stdout.write(` ${label}${suffix}${hint}: `);
72824
- 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
+ });
72825
74773
  rl.once("line", (line2) => {
72826
74774
  rl.close();
72827
74775
  resolve8(line2.trim() || (required ? placeholder : ""));
@@ -72832,7 +74780,7 @@ function askPassword(label) {
72832
74780
  return new Promise((resolve8) => {
72833
74781
  const stdin = process.stdin;
72834
74782
  if (typeof stdin.setRawMode === "function") {
72835
- process.stdout.write(` ${label}${import_picocolors26.default.dim(" [optional]")}: `);
74783
+ process.stdout.write(` ${label}${import_picocolors27.default.dim(" [optional]")}: `);
72836
74784
  stdin.setRawMode(true);
72837
74785
  stdin.resume();
72838
74786
  let value = "";
@@ -72863,8 +74811,15 @@ function askPassword(label) {
72863
74811
  });
72864
74812
  }
72865
74813
  function buildEsPatch(opts) {
72866
- if (!opts.indexPattern) throw new Error("Index pattern is required for elasticsearch");
72867
- 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
+ }
72868
74823
  if (opts.url) patch["url"] = opts.url;
72869
74824
  if (opts.username) patch["username"] = opts.username;
72870
74825
  if (opts.password) patch["password"] = opts.password;
@@ -72885,7 +74840,11 @@ function buildGrafanaPatch(opts) {
72885
74840
  if (opts.url) patch["url"] = opts.url;
72886
74841
  if (opts.username) patch["username"] = opts.username;
72887
74842
  if (opts.password) patch["password"] = opts.password;
72888
- 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
+ }
72889
74848
  return patch;
72890
74849
  }
72891
74850
  function buildRedisPatch(opts) {
@@ -72904,7 +74863,10 @@ async function probe(type, opts) {
72904
74863
  password: opts.password
72905
74864
  });
72906
74865
  const controller = new AbortController();
72907
- 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
+ );
72908
74870
  try {
72909
74871
  return await client.health(controller.signal);
72910
74872
  } catch (err) {
@@ -72931,7 +74893,9 @@ async function probe(type, opts) {
72931
74893
  if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
72932
74894
  const headers = { "Content-Type": "application/json" };
72933
74895
  if (opts.username && opts.password) {
72934
- const encoded = Buffer.from(`${opts.username}:${opts.password}`).toString("base64");
74896
+ const encoded = Buffer.from(`${opts.username}:${opts.password}`).toString(
74897
+ "base64"
74898
+ );
72935
74899
  headers["Authorization"] = `Basic ${encoded}`;
72936
74900
  }
72937
74901
  const res = await fetch(`${opts.url.replace(/\/$/, "")}/api/health`, {
@@ -72975,15 +74939,24 @@ function tcpProbe(host, port, timeoutMs = 3e3) {
72975
74939
  }
72976
74940
  function printSummary(type, opts) {
72977
74941
  const lines = [];
72978
- if (opts.url) lines.push(` url: ${redactUrl(opts.url)}`);
72979
- if (opts.username) lines.push(` username: ${opts.username}`);
72980
- if (opts.password) lines.push(` password: ${"\u2022".repeat(Math.min(opts.password.length, 8))}`);
72981
- if (opts.indexPattern) lines.push(` index-pattern: ${opts.indexPattern}`);
72982
- if (opts.service) lines.push(` service: ${opts.service}`);
72983
- if (opts.database) lines.push(` database: ${opts.database}`);
72984
- if (opts.collections) lines.push(` collections: ${opts.collections}`);
72985
- if (opts.dashboard) lines.push(` dashboard: ${opts.dashboard}`);
72986
- 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")));
72987
74960
  }
72988
74961
  function isGitTracked(filePath, cwd) {
72989
74962
  try {
@@ -72996,6 +74969,119 @@ function isGitTracked(filePath, cwd) {
72996
74969
  return false;
72997
74970
  }
72998
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
+ }
72999
75085
  function redactUrl(raw) {
73000
75086
  try {
73001
75087
  const u = new URL(raw);
@@ -73015,11 +75101,14 @@ var import_node_child_process6 = require("child_process");
73015
75101
  var import_node_fs7 = require("fs");
73016
75102
  var import_node_util4 = require("util");
73017
75103
  var import_node_path9 = require("path");
73018
- var import_picocolors27 = __toESM(require_picocolors(), 1);
75104
+ var import_picocolors28 = __toESM(require_picocolors(), 1);
73019
75105
  var execFileAsync = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
73020
75106
  var unlinkAsync = (0, import_node_util4.promisify)(import_node_fs7.unlink);
73021
75107
  var SPAWNED_HOST_FILE2 = "spawned-host.json";
73022
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));
73023
75112
  async function runStop(opts) {
73024
75113
  try {
73025
75114
  if (opts.all) {
@@ -73029,30 +75118,30 @@ async function runStop(opts) {
73029
75118
  const root = findRepoRoot(cwd) ?? cwd;
73030
75119
  const hostUrl = readSourceHostUrl(root);
73031
75120
  if (!hostUrl) {
73032
- 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)."));
73033
75122
  return 0;
73034
75123
  }
73035
75124
  return await stopHost(root, hostUrl);
73036
75125
  } catch (err) {
73037
- console.error(import_picocolors27.default.red(err.message));
75126
+ console.error(import_picocolors28.default.red(err.message));
73038
75127
  return 1;
73039
75128
  }
73040
75129
  }
73041
75130
  async function stopHost(root, hostUrl) {
73042
75131
  const alive = await isHostHealthy(hostUrl);
73043
75132
  if (!alive) {
73044
- console.log(import_picocolors27.default.dim(`Host ${hostUrl} is already stopped.`));
75133
+ console.log(import_picocolors28.default.dim(`Host ${hostUrl} is already stopped.`));
73045
75134
  return 0;
73046
75135
  }
73047
75136
  const port = extractPort(hostUrl);
73048
75137
  if (port === null) {
73049
- 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}`));
73050
75139
  return 1;
73051
75140
  }
73052
75141
  const spawned = readSpawnedHost(root);
73053
75142
  if (spawned === null) {
73054
75143
  console.error(
73055
- import_picocolors27.default.red(
75144
+ import_picocolors28.default.red(
73056
75145
  `No ownership record found (.horus/${SPAWNED_HOST_FILE2} absent). Horus will not stop a host it did not spawn.`
73057
75146
  )
73058
75147
  );
@@ -73060,12 +75149,12 @@ async function stopHost(root, hostUrl) {
73060
75149
  }
73061
75150
  const recordError = validateSpawnedRecord(spawned);
73062
75151
  if (recordError !== null) {
73063
- 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.`));
73064
75153
  return 1;
73065
75154
  }
73066
75155
  if (spawned.port !== port) {
73067
75156
  console.error(
73068
- import_picocolors27.default.red(
75157
+ import_picocolors28.default.red(
73069
75158
  `Ownership record port (${spawned.port}) does not match host URL port (${port}). Record may be stale.`
73070
75159
  )
73071
75160
  );
@@ -73073,7 +75162,7 @@ async function stopHost(root, hostUrl) {
73073
75162
  }
73074
75163
  if (spawned.root !== root) {
73075
75164
  console.error(
73076
- import_picocolors27.default.red(
75165
+ import_picocolors28.default.red(
73077
75166
  `Ownership record root (${spawned.root}) does not match resolved root (${root}). Record may be stale.`
73078
75167
  )
73079
75168
  );
@@ -73081,17 +75170,21 @@ async function stopHost(root, hostUrl) {
73081
75170
  }
73082
75171
  const info = await getProcessInfo(spawned.pid);
73083
75172
  if (info === null) {
73084
- console.error(import_picocolors27.default.red(`Process pid ${spawned.pid} is no longer running.`));
73085
- 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;
73086
75179
  }
73087
75180
  const portStr = String(port);
73088
- const axonHostPortRe = new RegExp(
73089
- `(?:^|\\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|$)`
73090
75183
  );
73091
- if (!axonHostPortRe.test(info.args)) {
75184
+ if (!hostPortRe.test(info.args)) {
73092
75185
  console.error(
73093
- import_picocolors27.default.red(
73094
- `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.`
73095
75188
  )
73096
75189
  );
73097
75190
  return 1;
@@ -73099,30 +75192,51 @@ async function stopHost(root, hostUrl) {
73099
75192
  const startTs = new Date(spawned.startedAt).getTime();
73100
75193
  const recordedAgeS = Math.round((Date.now() - startTs) / 1e3);
73101
75194
  if (!Number.isFinite(info.etimeSeconds)) {
73102
- 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.`));
73103
75196
  return 1;
73104
75197
  }
73105
75198
  if (Math.abs(info.etimeSeconds - recordedAgeS) > START_TIME_TOLERANCE_S) {
73106
75199
  console.error(
73107
- import_picocolors27.default.red(
75200
+ import_picocolors28.default.red(
73108
75201
  `Pid ${spawned.pid} age mismatch: record says ~${recordedAgeS}s, process reports ${info.etimeSeconds}s elapsed. Possible PID reuse \u2014 aborting for safety.`
73109
75202
  )
73110
75203
  );
73111
75204
  return 1;
73112
75205
  }
75206
+ let signaled = false;
73113
75207
  try {
73114
75208
  process.kill(spawned.pid, "SIGTERM");
73115
- console.log(
73116
- `${import_picocolors27.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors27.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
73117
- );
75209
+ signaled = true;
73118
75210
  } catch (err) {
73119
75211
  if (err.code === "ESRCH") {
73120
- 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.`));
73121
75213
  } else {
73122
- 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}`));
73123
75215
  return 1;
73124
75216
  }
73125
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
+ );
75234
+ return 1;
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
+ );
75239
+ }
73126
75240
  try {
73127
75241
  await unlinkAsync((0, import_node_path9.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
73128
75242
  } catch {
@@ -73133,7 +75247,7 @@ async function stopAll() {
73133
75247
  const registry = readRegistry();
73134
75248
  const projects2 = Object.entries(registry.projects);
73135
75249
  if (projects2.length === 0) {
73136
- console.log(import_picocolors27.default.dim("No registered projects."));
75250
+ console.log(import_picocolors28.default.dim("No registered projects."));
73137
75251
  return 0;
73138
75252
  }
73139
75253
  let stopped = 0;
@@ -73143,17 +75257,17 @@ async function stopAll() {
73143
75257
  if (!hostUrl) continue;
73144
75258
  const alive = await isHostHealthy(hostUrl);
73145
75259
  if (!alive) continue;
73146
- 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})`)}`);
73147
75261
  const code = await stopHost(entry2.root, hostUrl);
73148
75262
  if (code === 0) stopped++;
73149
75263
  else failed++;
73150
75264
  }
73151
75265
  if (stopped === 0 && failed === 0) {
73152
- 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."));
73153
75267
  } else {
73154
75268
  console.log(
73155
75269
  `
73156
- 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`) : ""}.`
73157
75271
  );
73158
75272
  }
73159
75273
  return failed > 0 ? 1 : 0;
@@ -73205,12 +75319,12 @@ function extractPort(hostUrl) {
73205
75319
 
73206
75320
  // ../../packages/cli/src/commands/hosts.ts
73207
75321
  init_cjs_shims();
73208
- var import_picocolors28 = __toESM(require_picocolors(), 1);
75322
+ var import_picocolors29 = __toESM(require_picocolors(), 1);
73209
75323
  async function runHosts() {
73210
75324
  const registry = readRegistry();
73211
75325
  const projects2 = Object.entries(registry.projects);
73212
75326
  if (projects2.length === 0) {
73213
- 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."));
73214
75328
  return 0;
73215
75329
  }
73216
75330
  const rows = [];
@@ -73227,29 +75341,29 @@ async function runHosts() {
73227
75341
  });
73228
75342
  const anyHost = rows.some((r) => r.hostUrl !== null);
73229
75343
  if (!anyHost) {
73230
- 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."));
73231
75345
  return 0;
73232
75346
  }
73233
75347
  console.log("");
73234
75348
  for (const row of rows) {
73235
75349
  if (row.hostUrl === null) continue;
73236
- 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");
73237
75351
  const port = extractPort2(row.hostUrl) ?? "?";
73238
75352
  console.log(
73239
- ` ${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)}`
73240
75354
  );
73241
75355
  }
73242
75356
  console.log("");
73243
75357
  const noHost = rows.filter((r) => r.hostUrl === null);
73244
75358
  if (noHost.length > 0) {
73245
75359
  for (const row of noHost) {
73246
- 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)}`);
73247
75361
  }
73248
75362
  console.log("");
73249
75363
  }
73250
75364
  const running = rows.filter((r) => r.healthy).length;
73251
75365
  console.log(
73252
- import_picocolors28.default.dim(
75366
+ import_picocolors29.default.dim(
73253
75367
  `${running} running \xB7 horus stop to reap \xB7 horus stop --all to stop everything`
73254
75368
  )
73255
75369
  );
@@ -73266,12 +75380,12 @@ function extractPort2(hostUrl) {
73266
75380
 
73267
75381
  // ../../packages/cli/src/commands/doctor.ts
73268
75382
  init_cjs_shims();
73269
- var import_picocolors29 = __toESM(require_picocolors(), 1);
75383
+ var import_picocolors30 = __toESM(require_picocolors(), 1);
73270
75384
  var DEFAULT_DB_URL3 = "postgresql://horus:horus@localhost:5433/horus";
73271
75385
  function mark2(status) {
73272
- if (status === "pass") return import_picocolors29.default.green("\u2713");
73273
- if (status === "warn") return import_picocolors29.default.yellow("~");
73274
- 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");
73275
75389
  }
73276
75390
  async function runDoctor(opts) {
73277
75391
  const cwd = opts?.cwd ?? process.cwd();
@@ -73301,7 +75415,7 @@ async function runDoctor(opts) {
73301
75415
  const project = file.project;
73302
75416
  const repos = project["repositories"];
73303
75417
  const hasHost = repos?.some(
73304
- (r) => r["axon"]?.["hostUrl"]
75418
+ (r) => r["source"]?.["hostUrl"] ?? r["axon"]?.["hostUrl"]
73305
75419
  );
73306
75420
  if (hasHost) {
73307
75421
  checks.push({ label: "Source-intelligence host", status: "pass", detail: "configured" });
@@ -73310,7 +75424,7 @@ async function runDoctor(opts) {
73310
75424
  label: "Source-intelligence host",
73311
75425
  status: "warn",
73312
75426
  detail: "not configured",
73313
- next: "run `horus index` to analyze this repo and start a host, or pass --axon <url> to `horus init`"
75427
+ next: "run `horus index` to analyze this repo and start a host, or pass --source <url> to `horus init`"
73314
75428
  });
73315
75429
  }
73316
75430
  } catch {
@@ -73498,11 +75612,11 @@ async function runDoctor(opts) {
73498
75612
  write(JSON.stringify(output, null, 2));
73499
75613
  return hasFailure ? 1 : 0;
73500
75614
  }
73501
- write(import_picocolors29.default.bold("\nHorus readiness check\n"));
75615
+ write(import_picocolors30.default.bold("\nHorus readiness check\n"));
73502
75616
  for (const check of checks) {
73503
- 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)}`);
73504
75618
  if (check.next) {
73505
- write(` ${import_picocolors29.default.dim("\u2192 " + check.next)}`);
75619
+ write(` ${import_picocolors30.default.dim("\u2192 " + check.next)}`);
73506
75620
  }
73507
75621
  }
73508
75622
  write("");
@@ -73511,29 +75625,34 @@ async function runDoctor(opts) {
73511
75625
 
73512
75626
  // ../../packages/cli/src/commands/providers-doctor.ts
73513
75627
  init_cjs_shims();
73514
- var import_picocolors30 = __toESM(require_picocolors(), 1);
75628
+ var import_node_child_process7 = require("child_process");
75629
+ var import_picocolors31 = __toESM(require_picocolors(), 1);
73515
75630
  function statusMark(status) {
73516
- if (status === "ready") return import_picocolors30.default.green("\u2713");
73517
- if (status === "installed") return import_picocolors30.default.yellow("~");
73518
- 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");
73519
75634
  }
73520
75635
  function statusLabel(status) {
73521
- if (status === "ready") return import_picocolors30.default.green("ready");
73522
- if (status === "installed") return import_picocolors30.default.yellow("installed (not configured)");
73523
- return import_picocolors30.default.dim("not found on PATH");
73524
- }
73525
- function buildProviderResults(registry) {
73526
- return registry.providers.map((p) => ({
73527
- id: p.id,
73528
- status: "unavailable",
73529
- detail: `install the ${p.displayName} binary to use this provider`
73530
- }));
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));
73531
75650
  }
73532
75651
  async function runProvidersDoctorCommand(opts) {
73533
75652
  const registry = opts?.registry ?? DEFAULT_LOCAL_PROVIDER_REGISTRY;
73534
75653
  const write = opts?.write ?? ((line2) => console.log(line2));
73535
- const results = buildProviderResults(registry);
73536
- 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"));
73537
75656
  for (const result of results) {
73538
75657
  const descriptor = registry.get(result.id);
73539
75658
  const name = descriptor?.displayName ?? result.id;
@@ -73541,12 +75660,24 @@ async function runProvidersDoctorCommand(opts) {
73541
75660
  ` ${statusMark(result.status)} ${result.id.padEnd(8)} ${name.padEnd(22)} ${statusLabel(result.status)}`
73542
75661
  );
73543
75662
  if (result.status !== "ready" && result.detail) {
73544
- write(` ${import_picocolors30.default.dim("\u2192 " + result.detail)}`);
75663
+ write(` ${import_picocolors31.default.dim("\u2192 " + result.detail)}`);
73545
75664
  }
73546
75665
  }
73547
75666
  write("");
73548
- write(import_picocolors30.default.dim(" Detection not yet implemented \u2014 install the provider binary first."));
73549
- 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
+ }
73550
75681
  write("");
73551
75682
  return 0;
73552
75683
  }
@@ -73555,7 +75686,7 @@ async function runProvidersDoctorCommand(opts) {
73555
75686
  init_cjs_shims();
73556
75687
  var import_node_fs8 = require("fs");
73557
75688
  var import_node_path10 = require("path");
73558
- var import_picocolors31 = __toESM(require_picocolors(), 1);
75689
+ var import_picocolors32 = __toESM(require_picocolors(), 1);
73559
75690
  function configTemplate(name, repoPath) {
73560
75691
  return `export default {
73561
75692
  database: {
@@ -73566,7 +75697,7 @@ function configTemplate(name, repoPath) {
73566
75697
  repositories: [{
73567
75698
  name: '${name}',
73568
75699
  path: '${repoPath}',
73569
- axon: { hostUrl: 'http://127.0.0.1:8420' },
75700
+ source: { hostUrl: 'http://127.0.0.1:8420' },
73570
75701
  }],
73571
75702
  environments: [{
73572
75703
  name: 'production',
@@ -73581,39 +75712,66 @@ function configTemplate(name, repoPath) {
73581
75712
  };
73582
75713
  `;
73583
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
+ }
73584
75730
  async function runGenerateConfig(opts) {
73585
75731
  const log = opts.write ?? ((line2) => console.log(line2));
73586
75732
  const cwd = opts.cwd ?? process.cwd();
73587
- const outPath = (0, import_node_path10.resolve)(cwd, opts.out ?? "horus.config.js");
73588
- const name = opts.name ?? "my-project";
73589
- 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}`;
73590
75740
  if ((0, import_node_fs8.existsSync)(outPath) && !opts.force) {
73591
- log(`${import_picocolors31.default.red("\u2717")} ${outPath} already exists`);
73592
- 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"));
73593
75743
  return 1;
73594
75744
  }
73595
75745
  try {
73596
75746
  (0, import_node_fs8.mkdirSync)((0, import_node_path10.dirname)(outPath), { recursive: true });
73597
75747
  (0, import_node_fs8.writeFileSync)(outPath, configTemplate(name, repoPath), "utf8");
73598
75748
  } catch (err) {
73599
- 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}`);
73600
75750
  return 1;
73601
75751
  }
73602
- log(`${import_picocolors31.default.green("\u2713")} Created ${outPath}`);
73603
- log(import_picocolors31.default.dim(` project: ${name}`));
73604
- log(import_picocolors31.default.dim(` repo: ${repoPath}`));
73605
- 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
+ }
73606
75764
  return 0;
73607
75765
  }
73608
75766
 
73609
75767
  // ../../packages/cli/src/commands/readiness.ts
73610
75768
  init_cjs_shims();
73611
- var import_picocolors32 = __toESM(require_picocolors(), 1);
75769
+ var import_picocolors33 = __toESM(require_picocolors(), 1);
73612
75770
  var DEFAULT_DB_URL4 = "postgresql://horus:horus@localhost:5433/horus";
73613
75771
  function mark3(status) {
73614
- if (status === "pass") return import_picocolors32.default.green("\u2713");
73615
- if (status === "warn") return import_picocolors32.default.yellow("~");
73616
- 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");
73617
75775
  }
73618
75776
  async function runReadiness(opts) {
73619
75777
  const cwd = opts?.cwd ?? process.cwd();
@@ -73676,7 +75834,7 @@ async function runReadiness(opts) {
73676
75834
  status: "warn",
73677
75835
  blocking: false,
73678
75836
  detail: "not installed \u2014 source intelligence unavailable",
73679
- next: `uv tool install axoniq==${PINNED_SOURCE_VERSION}`
75837
+ next: `pip install horus-source`
73680
75838
  });
73681
75839
  } else if (sourceVersion !== PINNED_SOURCE_VERSION) {
73682
75840
  checks.push({
@@ -73684,7 +75842,7 @@ async function runReadiness(opts) {
73684
75842
  status: "warn",
73685
75843
  blocking: false,
73686
75844
  detail: `version mismatch (installed: ${sourceVersion}, required: ${PINNED_SOURCE_VERSION})`,
73687
- next: `uv tool install axoniq==${PINNED_SOURCE_VERSION}`
75845
+ next: `pip install horus-source`
73688
75846
  });
73689
75847
  } else {
73690
75848
  checks.push({
@@ -73768,20 +75926,20 @@ async function runReadiness(opts) {
73768
75926
  }
73769
75927
  const blockingChecks = checks.filter((c) => c.blocking);
73770
75928
  const optionalChecks = checks.filter((c) => !c.blocking);
73771
- write(import_picocolors32.default.bold("\nHorus release readiness\n"));
73772
- write(import_picocolors32.default.bold(" Blocking"));
75929
+ write(import_picocolors33.default.bold("\nHorus release readiness\n"));
75930
+ write(import_picocolors33.default.bold(" Blocking"));
73773
75931
  for (const check of blockingChecks) {
73774
- 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)}`);
73775
75933
  if (check.next) {
73776
- write(` ${import_picocolors32.default.dim("\u2192 " + check.next)}`);
75934
+ write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
73777
75935
  }
73778
75936
  }
73779
75937
  write("");
73780
- write(import_picocolors32.default.bold(" Optional"));
75938
+ write(import_picocolors33.default.bold(" Optional"));
73781
75939
  for (const check of optionalChecks) {
73782
- 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)}`);
73783
75941
  if (check.next) {
73784
- write(` ${import_picocolors32.default.dim("\u2192 " + check.next)}`);
75942
+ write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
73785
75943
  }
73786
75944
  }
73787
75945
  write("");
@@ -73789,21 +75947,21 @@ async function runReadiness(opts) {
73789
75947
  const optionalWarns = optionalChecks.filter((c) => c.status === "warn").length;
73790
75948
  if (blockingFails.length === 0) {
73791
75949
  if (optionalWarns === 0) {
73792
- write(import_picocolors32.default.green(" Ready for demo/release."));
75950
+ write(import_picocolors33.default.green(" Ready for demo/release."));
73793
75951
  } else {
73794
75952
  write(
73795
- import_picocolors32.default.yellow(
75953
+ import_picocolors33.default.yellow(
73796
75954
  ` Ready for a basic demo. ${optionalWarns} optional item(s) not configured \u2014 investigation evidence will be limited.`
73797
75955
  )
73798
75956
  );
73799
75957
  }
73800
75958
  } else {
73801
75959
  write(
73802
- import_picocolors32.default.red(
75960
+ import_picocolors33.default.red(
73803
75961
  ` Not ready. ${blockingFails.length} blocking item(s) must be resolved before demo/release.`
73804
75962
  )
73805
75963
  );
73806
- 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."));
73807
75965
  }
73808
75966
  write("");
73809
75967
  return blockingFails.length > 0 ? 1 : 0;
@@ -73828,9 +75986,14 @@ Examples:
73828
75986
  program2.command("setup").description("Verify prerequisites (source-intelligence backend + Postgres) and guide any fixes").option("-c, --config <path>", "path to horus.config.ts").action(async (opts) => {
73829
75987
  process.exitCode = await runSetup(opts);
73830
75988
  });
73831
- program2.command("init").description("Create a local .horus/config.json for this repo and register it").option("--name <name>", "project name (default: repo directory name)").option("--env <name>", "environment name (default: production)").option("--axon <url>", "Source-intelligence host URL for this repo (e.g. http://127.0.0.1:8420)").option("--path <dir>", "repository root (default: nearest git root, else cwd)").action(
75989
+ program2.command("init").description("Create a local .horus/config.json for this repo and register it").option("--name <name>", "project name (default: repo directory name)").option("--env <name>", "environment name (default: production)").option("--source <url>", "source-intelligence host URL for this repo (e.g. http://127.0.0.1:8420)").addOption(new Option("--axon <url>", "deprecated alias for --source").hideHelp()).option("--path <dir>", "repository root (default: nearest git root, else cwd)").action(
73832
75990
  async (opts) => {
73833
- process.exitCode = await runInit(opts);
75991
+ process.exitCode = await runInit({
75992
+ name: opts.name,
75993
+ env: opts.env,
75994
+ source: opts.source ?? opts.axon,
75995
+ path: opts.path
75996
+ });
73834
75997
  }
73835
75998
  ).addHelpText("after", `
73836
75999
  Examples:
@@ -73849,7 +76012,7 @@ Examples:
73849
76012
  });
73850
76013
  program2.command("connect <type>").description(
73851
76014
  "Add or update a runtime connector (elasticsearch / mongodb / grafana / redis) in .horus/config.json"
73852
- ).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(
73853
76016
  async (type, opts) => {
73854
76017
  process.exitCode = await runConnect(type, {
73855
76018
  env: opts.env,
@@ -73898,9 +76061,16 @@ Examples:
73898
76061
  process.exitCode = await runIndex(opts);
73899
76062
  }
73900
76063
  );
73901
- 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) => {
73902
- process.exitCode = await runQueues(name, { config: opts.config, project: opts.project });
73903
- });
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
+ );
73904
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(
73905
76075
  "--service <name>",
73906
76076
  "service name to scope runtime logs, e.g. leadcall-api-prod"
@@ -73995,23 +76165,29 @@ Examples:
73995
76165
  horus investigations
73996
76166
  horus investigations -n 20
73997
76167
  `);
73998
- 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").action(async (id, opts) => {
73999
- process.exitCode = await runReplay(id, { config: opts.config, format: opts.format });
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 });
74000
76170
  }).addHelpText("after", `
74001
76171
  Examples:
74002
76172
  horus replay <id>
74003
76173
  horus replay <id> --format markdown
74004
76174
  horus replay <id> --format json
76175
+ horus replay <id> --ai
76176
+ horus replay <id> --ai --ai-model claude-sonnet-4-6
76177
+ horus replay <id> --ai --refresh-ai
74005
76178
 
74006
76179
  (Use 'horus investigations' to list saved investigation ids.)
74007
76180
  `);
74008
- 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").action(async (id, opts) => {
74009
- process.exitCode = await runPostmortem(id, { config: opts.config, output: opts.output, force: opts.force });
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 });
74010
76183
  }).addHelpText("after", `
74011
76184
  Examples:
74012
76185
  horus postmortem <id>
74013
76186
  horus postmortem <id> --output ./postmortem.md
74014
76187
  horus postmortem <id> --output ./postmortem.md --force
76188
+ horus postmortem <id> --ai-summary
76189
+ horus postmortem <id> --output ./postmortem.md --ai-summary --ai-model claude-sonnet-4-6
76190
+ horus postmortem <id> --ai-summary --refresh-ai
74015
76191
 
74016
76192
  (Use 'horus investigations' to list saved investigation ids.)
74017
76193
  `);