@merittdev/horus 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +911 -285
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -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 = true ? "0.1.14" : "dev";
50317
+ var HORUS_VERSION = true ? "0.1.16" : "dev";
50318
50318
  var PINNED_AXON_VERSION = "1.0.7";
50319
50319
  var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
50320
50320
 
@@ -69193,7 +69193,7 @@ function buildRuntimeSourceStatus(evidence2, connectors) {
69193
69193
  const metricsFailed = metricsConfigured && !connectors.metricsCollected;
69194
69194
  const stateCount = evidence2.filter((e) => e.source === "state").length;
69195
69195
  const stateConfigured = !!(connectors.redis || connectors.mongodb);
69196
- const queueConfigured = evidence2.some((e) => e.source === "queue");
69196
+ const queueConfigured = connectors.queue ?? evidence2.some((e) => e.source === "queue");
69197
69197
  const queueCount = evidence2.filter((e) => e.kind === "queue-state").length;
69198
69198
  return {
69199
69199
  sources: [
@@ -70120,11 +70120,15 @@ async function investigate(input, deps) {
70120
70120
  const queueRuntimeEvIdsByQueue = /* @__PURE__ */ new Map();
70121
70121
  const queueBacklogEvIdsByQueue = /* @__PURE__ */ new Map();
70122
70122
  const queueStarvationEvIdsByQueue = /* @__PURE__ */ new Map();
70123
- if (deps.queue && queueHits.length > 0) {
70123
+ if (deps.queue) {
70124
70124
  try {
70125
- const queueNames2 = [...new Set(queueHits.map((e) => e.queueName))];
70126
- queueRuntimeState = await deps.queue.analyzeQueues({ queueNames: queueNames2 });
70127
- for (const s of analyzeQueueRuntime(queueRuntimeState)) {
70125
+ const staticNames = [...new Set(queueHits.map((e) => e.queueName))];
70126
+ const discovered = await deps.queue.discoverQueues().catch(() => []);
70127
+ const queueNames2 = [.../* @__PURE__ */ new Set([...staticNames, ...discovered])];
70128
+ if (queueNames2.length > 0) {
70129
+ queueRuntimeState = await deps.queue.analyzeQueues({ queueNames: queueNames2 });
70130
+ }
70131
+ for (const s of analyzeQueueRuntime(queueRuntimeState ?? { prefix: "", collectedAt: "", queues: [] })) {
70128
70132
  const ev = mkEv("queue-state", s.title, s.payload, { queueName: s.queueName }, s.timestamp, s.relevance);
70129
70133
  queueRuntimeEvIds.push(ev.id);
70130
70134
  const perQueue = queueRuntimeEvIdsByQueue.get(s.queueName) ?? [];
@@ -70571,10 +70575,21 @@ async function investigate(input, deps) {
70571
70575
  causeChains: causeChains.length > 0 ? causeChains : void 0,
70572
70576
  ...recentChanges !== void 0 ? { recentChanges } : {}
70573
70577
  };
70574
- const connectorFlags = deps.connectors ? { ...deps.connectors, metricsCollected, metricsFailureReason, logsCollected, logsCompatibilityError, sinceProvided: input.since !== void 0 } : {
70578
+ const connectorFlags = deps.connectors ? {
70579
+ ...deps.connectors,
70580
+ // The queue connector is configured iff a BullMQ provider was constructed
70581
+ // (queueForEnv returns null without a bullmq/queues Redis DB) — HOR-205.
70582
+ queue: deps.queue != null,
70583
+ metricsCollected,
70584
+ metricsFailureReason,
70585
+ logsCollected,
70586
+ logsCompatibilityError,
70587
+ sinceProvided: input.since !== void 0
70588
+ } : {
70575
70589
  elasticsearch: deps.logs != null,
70576
70590
  mongodb: deps.mongo != null,
70577
70591
  grafana: deps.metrics != null,
70592
+ queue: deps.queue != null,
70578
70593
  metricsCollected,
70579
70594
  metricsFailureReason,
70580
70595
  logsCollected,
@@ -73198,253 +73213,6 @@ async function runIndex(opts) {
73198
73213
  // ../../packages/cli/src/commands/queues.ts
73199
73214
  init_cjs_shims();
73200
73215
  var import_picocolors4 = __toESM(require_picocolors(), 1);
73201
- async function runQueues(name, opts) {
73202
- try {
73203
- const config = await loadConfig(opts.config, { name: opts.name });
73204
- const { db, sql: sql2 } = createDb(config.database.url);
73205
- try {
73206
- let project = opts.project;
73207
- if (project === void 0) {
73208
- try {
73209
- project = resolveEnvironment(config, { project: opts.project }).project;
73210
- } catch {
73211
- }
73212
- }
73213
- const rows = await listQueueEdges(db, { project, queueName: name });
73214
- if (opts.json) {
73215
- const topology = topologyToJson(buildQueueMap(rows));
73216
- const out = { project: project ?? null, topology };
73217
- if (opts.live) out["live"] = await gatherLiveState(config, rows, name);
73218
- console.log(JSON.stringify(out, null, 2));
73219
- return 0;
73220
- }
73221
- console.log(
73222
- import_picocolors4.default.bold("Queue topology") + import_picocolors4.default.dim(" \xB7 source: code / source intelligence \xB7 static (run horus index to refresh)")
73223
- );
73224
- console.log("");
73225
- if (rows.length === 0) {
73226
- console.log(import_picocolors4.default.dim(" No queue edges indexed. Run: horus index"));
73227
- } else {
73228
- const byQueue = buildQueueMap(rows);
73229
- printTopology(byQueue);
73230
- }
73231
- console.log("");
73232
- if (opts.live) {
73233
- await runLiveMode(config, rows, name);
73234
- } else {
73235
- console.log(
73236
- import_picocolors4.default.dim(
73237
- " Tip: run horus queues --live to show real-time Redis/BullMQ depths and failed-job counts."
73238
- )
73239
- );
73240
- }
73241
- } finally {
73242
- await sql2.end();
73243
- }
73244
- return 0;
73245
- } catch (err) {
73246
- console.error(import_picocolors4.default.red(err.message));
73247
- return 1;
73248
- }
73249
- }
73250
- function buildQueueMap(rows) {
73251
- const byQueue = /* @__PURE__ */ new Map();
73252
- for (const row of rows) {
73253
- const existing = byQueue.get(row.queueName);
73254
- if (existing) {
73255
- existing.push(row);
73256
- } else {
73257
- byQueue.set(row.queueName, [row]);
73258
- }
73259
- }
73260
- return byQueue;
73261
- }
73262
- function endpoints(edges, symKey, fileKey) {
73263
- const seen = /* @__PURE__ */ new Map();
73264
- for (const e of edges) {
73265
- const symbol = e[symKey];
73266
- if (!symbol) continue;
73267
- if (!seen.has(symbol)) seen.set(symbol, { symbol, file: e[fileKey] ?? null });
73268
- }
73269
- return [...seen.values()];
73270
- }
73271
- function topologyToJson(byQueue) {
73272
- return [...byQueue.entries()].map(([queueName, edges]) => ({
73273
- queueName,
73274
- producers: endpoints(edges, "producerSymbol", "producerFile"),
73275
- workers: endpoints(edges, "workerSymbol", "workerFile")
73276
- }));
73277
- }
73278
- async function gatherLiveState(config, rows, nameFilter) {
73279
- let renv;
73280
- try {
73281
- renv = resolveEnvironment(config);
73282
- } catch (err) {
73283
- return { ok: false, error: err.message };
73284
- }
73285
- const queueProvider = queueForEnv(renv);
73286
- if (!queueProvider) return { ok: false, error: "Redis not configured" };
73287
- try {
73288
- const health = await queueProvider.health();
73289
- if (!health.ok) return { ok: false, error: health.detail };
73290
- const staticNames = new Set(buildQueueMap(rows).keys());
73291
- let queueNames;
73292
- if (nameFilter !== void 0) {
73293
- queueNames = [nameFilter];
73294
- } else {
73295
- const discovered = await queueProvider.discoverQueues().catch(() => []);
73296
- const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
73297
- queueNames = union2.size > 0 ? [...union2] : void 0;
73298
- }
73299
- const state = await queueProvider.analyzeQueues({ queueNames });
73300
- return {
73301
- ok: true,
73302
- prefix: state.prefix,
73303
- collectedAt: state.collectedAt,
73304
- queues: state.queues.map((q) => ({ ...q, runtimeOnly: !staticNames.has(q.queueName) }))
73305
- };
73306
- } finally {
73307
- await queueProvider.close().catch(() => {
73308
- });
73309
- }
73310
- }
73311
- function printTopology(byQueue) {
73312
- for (const [queueName, edges] of byQueue) {
73313
- console.log(import_picocolors4.default.bold(queueName));
73314
- const producerSet = /* @__PURE__ */ new Set();
73315
- const producerDetails = /* @__PURE__ */ new Map();
73316
- for (const edge of edges) {
73317
- if (edge.producerSymbol) {
73318
- producerSet.add(edge.producerSymbol);
73319
- if (edge.producerFile) producerDetails.set(edge.producerSymbol, edge.producerFile);
73320
- }
73321
- }
73322
- if (producerSet.size === 0) {
73323
- console.log(" producers: " + import_picocolors4.default.dim("none"));
73324
- } else {
73325
- const list = Array.from(producerSet).map((sym) => {
73326
- const file = producerDetails.get(sym);
73327
- return file ? `${sym} (${file})` : sym;
73328
- }).join(", ");
73329
- console.log(" producers: " + list);
73330
- }
73331
- const workerSet = /* @__PURE__ */ new Set();
73332
- const workerDetails = /* @__PURE__ */ new Map();
73333
- for (const edge of edges) {
73334
- if (edge.workerSymbol) {
73335
- workerSet.add(edge.workerSymbol);
73336
- if (edge.workerFile) workerDetails.set(edge.workerSymbol, edge.workerFile);
73337
- }
73338
- }
73339
- if (workerSet.size === 0) {
73340
- console.log(" workers: " + import_picocolors4.default.dim("none"));
73341
- } else {
73342
- const list = Array.from(workerSet).map((sym) => {
73343
- const file = workerDetails.get(sym);
73344
- return file ? `${sym} (${file})` : sym;
73345
- }).join(", ");
73346
- console.log(" workers: " + list);
73347
- }
73348
- console.log("");
73349
- }
73350
- }
73351
- async function runLiveMode(config, rows, nameFilter) {
73352
- let renv;
73353
- try {
73354
- renv = resolveEnvironment(config);
73355
- } catch (err) {
73356
- console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
73357
- console.log(import_picocolors4.default.yellow(` \u26A0 Cannot resolve environment: ${err.message}`));
73358
- return;
73359
- }
73360
- const queueProvider = queueForEnv(renv);
73361
- if (!queueProvider) {
73362
- console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
73363
- console.log(
73364
- import_picocolors4.default.yellow(" \u25CB Redis not configured \u2014 run: ") + import_picocolors4.default.bold("horus connect redis")
73365
- );
73366
- return;
73367
- }
73368
- let headerPrinted = false;
73369
- try {
73370
- const health = await queueProvider.health();
73371
- if (!health.ok) {
73372
- console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
73373
- console.log(import_picocolors4.default.red(` \u2717 Redis unreachable: ${health.detail}`));
73374
- return;
73375
- }
73376
- const staticNames = new Set(buildQueueMap(rows).keys());
73377
- let queueNames;
73378
- if (nameFilter !== void 0) {
73379
- queueNames = [nameFilter];
73380
- } else {
73381
- const discovered = await queueProvider.discoverQueues().catch(() => []);
73382
- const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
73383
- queueNames = union2.size > 0 ? [...union2] : void 0;
73384
- }
73385
- const state = await queueProvider.analyzeQueues({ queueNames });
73386
- const collectedAt = new Date(state.collectedAt).toLocaleTimeString();
73387
- console.log(
73388
- import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(` \xB7 source: Redis/BullMQ (prefix: ${state.prefix}) \xB7 collected: ${collectedAt}`)
73389
- );
73390
- headerPrinted = true;
73391
- console.log("");
73392
- if (state.queues.length === 0) {
73393
- console.log(import_picocolors4.default.dim(" No queues found in Redis."));
73394
- console.log(
73395
- import_picocolors4.default.dim(
73396
- " If queues exist under a custom prefix, set the BullMQ prefix in your connector config."
73397
- )
73398
- );
73399
- return;
73400
- }
73401
- printLiveTable(state.queues, staticNames);
73402
- } catch (err) {
73403
- if (!headerPrinted) {
73404
- console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
73405
- }
73406
- console.log(import_picocolors4.default.red(` \u2717 ${err.message}`));
73407
- } finally {
73408
- await queueProvider.close().catch(() => {
73409
- });
73410
- }
73411
- }
73412
- function printLiveTable(queues, staticNames = /* @__PURE__ */ new Set()) {
73413
- const nameWidth = Math.max(10, ...queues.map((q) => q.queueName.length));
73414
- const numWidth = 7;
73415
- const header = " " + "queue".padEnd(nameWidth) + " " + "waiting".padStart(numWidth) + " " + "active".padStart(numWidth) + " " + "failed".padStart(numWidth) + " " + "delayed".padStart(numWidth) + " " + "paused".padStart(numWidth);
73416
- console.log(import_picocolors4.default.dim(header));
73417
- console.log(import_picocolors4.default.dim(" " + "\u2500".repeat(header.length - 2)));
73418
- for (const q of queues) {
73419
- const hasIssue = q.failed > 0 || q.waiting > 100 || q.delayed > 50 || q.isPaused;
73420
- const color = hasIssue ? import_picocolors4.default.yellow : (s) => s;
73421
- 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));
73422
- console.log(color(row));
73423
- if (!staticNames.has(q.queueName)) {
73424
- console.log(import_picocolors4.default.dim(" runtime-only \xB7 no static producer/worker mapping"));
73425
- }
73426
- if (q.oldestWaitingMs !== void 0) {
73427
- const age = formatAge(q.oldestWaitingMs);
73428
- console.log(import_picocolors4.default.dim(` oldest waiting: ${age}`));
73429
- }
73430
- if (q.failedBreakdown && q.failedBreakdown.length > 0) {
73431
- for (const { reason, count } of q.failedBreakdown.slice(0, 3)) {
73432
- console.log(import_picocolors4.default.red(` \u2717 [${count}x] ${reason}`));
73433
- }
73434
- }
73435
- }
73436
- console.log("");
73437
- }
73438
- function formatAge(ms) {
73439
- if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
73440
- if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
73441
- if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
73442
- return `${Math.round(ms / 864e5)}d`;
73443
- }
73444
-
73445
- // ../../packages/cli/src/commands/investigate.ts
73446
- init_cjs_shims();
73447
- var import_picocolors5 = __toESM(require_picocolors(), 1);
73448
73216
 
73449
73217
  // ../../packages/ai/src/index.ts
73450
73218
  init_cjs_shims();
@@ -73681,6 +73449,15 @@ var AnthropicNarrativeProvider = class {
73681
73449
  const raw = await this.callApi(prompt);
73682
73450
  return parseOutput(raw, input, ceiling);
73683
73451
  }
73452
+ /**
73453
+ * HOR-211 — generic command-level interpretation. Reuses the same Messages API
73454
+ * call as render(), but with a caller-supplied command prompt (built by
73455
+ * buildInterpretationPrompt) instead of the investigation narrative prompt.
73456
+ * Returns the raw model text; the shared helper handles parsing/rendering.
73457
+ */
73458
+ async interpret(prompt) {
73459
+ return this.callApi(prompt);
73460
+ }
73684
73461
  async callApi(prompt) {
73685
73462
  const response = await fetch(`${this.baseUrl}/v1/messages`, {
73686
73463
  method: "POST",
@@ -73923,8 +73700,445 @@ init_cjs_shims();
73923
73700
  // ../../packages/ai/src/provider-execution.ts
73924
73701
  init_cjs_shims();
73925
73702
 
73926
- // ../../packages/cli/src/commands/investigate.ts
73927
- function extractEvidenceExcerpt(e) {
73703
+ // ../../packages/ai/src/interpretation.ts
73704
+ init_cjs_shims();
73705
+ var INTERPRETATION_GROUNDING_RULES = [
73706
+ "You are interpreting Horus evidence, not gathering new evidence.",
73707
+ "Do not invent files, commits, services, queues, logs, metrics, dashboards, owners, or timestamps.",
73708
+ "Use only the evidence provided above \u2014 any fact not present in it is a hallucination.",
73709
+ 'Clearly separate "Evidence used" from "Interpretation".',
73710
+ "State your confidence and call out any missing evidence that would change the conclusion.",
73711
+ "When suggesting next checks, prefer exact follow-up Horus commands."
73712
+ ];
73713
+ var PROMPT_KIND_GUIDANCE = {
73714
+ "change-risk": "Assess which changes carry the most risk and why, ranking by likely impact, grounded only in the evidence.",
73715
+ "timeline-narrative": "Narrate the ordering of changes and evidence into phases; flag suspicious ordering and gaps in coverage.",
73716
+ "blast-radius": "Explain severity, likely user impact, containment, and safe mitigations for the affected dependencies.",
73717
+ "system-explanation": "Explain how this system/subsystem works at onboarding quality, grounded in the evidence.",
73718
+ readiness: "Summarize readiness and risk, then prioritize what to do next before proceeding.",
73719
+ "evidence-summary": "Summarize what the evidence shows. Do not assert a root cause the evidence does not support."
73720
+ };
73721
+ function stringifyEvidence(evidence2) {
73722
+ if (typeof evidence2 === "string") return evidence2;
73723
+ try {
73724
+ return JSON.stringify(evidence2, null, 2);
73725
+ } catch {
73726
+ return String(evidence2);
73727
+ }
73728
+ }
73729
+ function buildInterpretationPrompt(req) {
73730
+ const guidance = PROMPT_KIND_GUIDANCE[req.promptKind] ?? PROMPT_KIND_GUIDANCE["evidence-summary"];
73731
+ const evidenceBlock = stringifyEvidence(req.evidence);
73732
+ const rules = INTERPRETATION_GROUNDING_RULES.map((r) => `- ${r}`).join("\n");
73733
+ return `You are the Horus AI interpretation layer for the \`${req.command}\` command.
73734
+ Task kind: ${req.promptKind}. ${guidance}
73735
+ ${req.userIntent ? `
73736
+ User intent: ${req.userIntent}
73737
+ ` : ""}
73738
+ Evidence used (this is the ONLY ground truth \u2014 interpret it, do not extend it):
73739
+ ${evidenceBlock}
73740
+
73741
+ Output contract:
73742
+ ${req.outputContract}
73743
+
73744
+ Rules:
73745
+ ${rules}
73746
+ - End with a "Confidence:" line (low | medium | high) and a "Next checks:" list of concrete Horus commands.`;
73747
+ }
73748
+ async function generateInterpretation(req, provider, opts) {
73749
+ const base2 = {
73750
+ ok: false,
73751
+ command: req.command,
73752
+ promptKind: req.promptKind,
73753
+ ...opts?.model ? { model: opts.model } : {}
73754
+ };
73755
+ if (!provider) {
73756
+ return {
73757
+ ...base2,
73758
+ warning: "No AI provider configured. Run `horus connect ai` or set ANTHROPIC_API_KEY to enable --ai."
73759
+ };
73760
+ }
73761
+ const prompt = buildInterpretationPrompt(req);
73762
+ try {
73763
+ const text2 = (await provider.interpret(prompt))?.trim() ?? "";
73764
+ if (!text2) {
73765
+ return { ...base2, provider: provider.name, warning: "AI provider returned an empty response." };
73766
+ }
73767
+ return {
73768
+ ok: true,
73769
+ command: req.command,
73770
+ promptKind: req.promptKind,
73771
+ provider: provider.name,
73772
+ ...opts?.model ? { model: opts.model } : {},
73773
+ text: text2
73774
+ };
73775
+ } catch (err) {
73776
+ return {
73777
+ ...base2,
73778
+ provider: provider.name,
73779
+ warning: `AI interpretation failed: ${err instanceof Error ? err.message : String(err)}`
73780
+ };
73781
+ }
73782
+ }
73783
+ function renderInterpretation(result) {
73784
+ const attribution = result.model ? ` (${result.provider ?? "ai"}, ${result.model})` : result.provider ? ` (${result.provider})` : "";
73785
+ const header = `\u2500\u2500 AI Interpretation${attribution} \u2500\u2500`;
73786
+ if (!result.ok) {
73787
+ return `${header}
73788
+ ${result.warning ?? "AI interpretation unavailable."}`;
73789
+ }
73790
+ return `${header}
73791
+ ${result.text ?? ""}`;
73792
+ }
73793
+
73794
+ // ../../packages/cli/src/lib/ai-provider.ts
73795
+ init_cjs_shims();
73796
+ var DEFAULT_MODEL = "claude-opus-4-8";
73797
+ async function resolveAiCredentials(opts) {
73798
+ let apiKey;
73799
+ let savedModel;
73800
+ try {
73801
+ const config = await loadConfig(opts.config);
73802
+ const ai = resolveAiSettings(config);
73803
+ apiKey = ai.anthropicApiKey;
73804
+ savedModel = ai.model;
73805
+ } catch {
73806
+ }
73807
+ apiKey = apiKey ?? process.env["ANTHROPIC_API_KEY"] ?? void 0;
73808
+ return { apiKey, model: opts.modelOverride ?? savedModel ?? DEFAULT_MODEL };
73809
+ }
73810
+ async function buildNarrativeProvider(opts) {
73811
+ let apiKey;
73812
+ let savedModel;
73813
+ try {
73814
+ const config = await loadConfig(opts.config);
73815
+ const ai = resolveAiSettings(config);
73816
+ apiKey = ai.anthropicApiKey;
73817
+ savedModel = ai.model;
73818
+ } catch {
73819
+ }
73820
+ const model = opts.modelOverride ?? savedModel ?? DEFAULT_MODEL;
73821
+ const provider = new AnthropicNarrativeProvider({
73822
+ model,
73823
+ ...apiKey !== void 0 ? { apiKey } : {}
73824
+ });
73825
+ return { provider, model };
73826
+ }
73827
+ async function renderAiInterpretation(opts) {
73828
+ const { config, modelOverride, provider: injected, command, evidence: evidence2, promptKind, outputContract, userIntent } = opts;
73829
+ const request = {
73830
+ command,
73831
+ evidence: evidence2,
73832
+ promptKind,
73833
+ outputContract,
73834
+ ...userIntent ? { userIntent } : {}
73835
+ };
73836
+ if (injected) {
73837
+ return generateInterpretation(request, injected);
73838
+ }
73839
+ const { apiKey, model } = await resolveAiCredentials({ config, modelOverride });
73840
+ if (!apiKey) {
73841
+ return generateInterpretation(request, null, { model });
73842
+ }
73843
+ const provider = new AnthropicNarrativeProvider({ model, apiKey });
73844
+ return generateInterpretation(request, provider, { model });
73845
+ }
73846
+
73847
+ // ../../packages/cli/src/commands/queues.ts
73848
+ var QUEUES_AI_CONTRACT = `Provide a clearly separated AI evidence narration with:
73849
+
73850
+ Evidence used
73851
+ - Queue names, waiting/active/failed/delayed counts, and producer/worker mappings Horus found
73852
+
73853
+ What stands out
73854
+ - Queues with high failed counts, unexpected backlog, or paused state
73855
+ - Runtime-only queues (live in Redis, no static producer/worker mapping)
73856
+ - Producer/worker mismatches or orphaned queues
73857
+
73858
+ What this may indicate
73859
+ - Use "may suggest", "is consistent with", or "could indicate" \u2014 never "proves"
73860
+ - Do not invent queue names, Redis keys, or job data not in the evidence
73861
+
73862
+ What is not proven
73863
+ - Claims about job contents, specific error messages, or data inside failed jobs
73864
+
73865
+ Next checks
73866
+ - Exact Horus commands or Redis/BullMQ checks to inspect next`;
73867
+ async function runQueues(name, opts) {
73868
+ try {
73869
+ const config = await loadConfig(opts.config, { name: opts.name });
73870
+ const { db, sql: sql2 } = createDb(config.database.url);
73871
+ try {
73872
+ let project = opts.project;
73873
+ if (project === void 0) {
73874
+ try {
73875
+ project = resolveEnvironment(config, { project: opts.project }).project;
73876
+ } catch {
73877
+ }
73878
+ }
73879
+ const rows = await listQueueEdges(db, { project, queueName: name });
73880
+ if (opts.json) {
73881
+ const topology = topologyToJson(buildQueueMap(rows));
73882
+ const out = { project: project ?? null, topology };
73883
+ if (opts.live) out["live"] = await gatherLiveState(config, rows, name);
73884
+ console.log(JSON.stringify(out, null, 2));
73885
+ return 0;
73886
+ }
73887
+ console.log(
73888
+ import_picocolors4.default.bold("Queue topology") + import_picocolors4.default.dim(" \xB7 source: code / source intelligence \xB7 static (run horus index to refresh)")
73889
+ );
73890
+ console.log("");
73891
+ if (rows.length === 0) {
73892
+ console.log(import_picocolors4.default.dim(" No queue edges indexed. Run: horus index"));
73893
+ } else {
73894
+ const byQueue = buildQueueMap(rows);
73895
+ printTopology(byQueue);
73896
+ }
73897
+ console.log("");
73898
+ if (opts.live) {
73899
+ await runLiveMode(config, rows, name, opts.ai ? {
73900
+ config: opts.config,
73901
+ aiModel: opts.aiModel,
73902
+ _aiProvider: opts._aiProvider
73903
+ } : void 0);
73904
+ } else {
73905
+ console.log(
73906
+ import_picocolors4.default.dim(
73907
+ " Tip: run horus queues --live to show real-time Redis/BullMQ depths and failed-job counts."
73908
+ )
73909
+ );
73910
+ if (opts.ai) {
73911
+ console.log(import_picocolors4.default.dim(" Tip: --ai is most useful with --live (horus queues --live --ai)."));
73912
+ }
73913
+ }
73914
+ } finally {
73915
+ await sql2.end();
73916
+ }
73917
+ return 0;
73918
+ } catch (err) {
73919
+ console.error(import_picocolors4.default.red(err.message));
73920
+ return 1;
73921
+ }
73922
+ }
73923
+ function buildQueueMap(rows) {
73924
+ const byQueue = /* @__PURE__ */ new Map();
73925
+ for (const row of rows) {
73926
+ const existing = byQueue.get(row.queueName);
73927
+ if (existing) {
73928
+ existing.push(row);
73929
+ } else {
73930
+ byQueue.set(row.queueName, [row]);
73931
+ }
73932
+ }
73933
+ return byQueue;
73934
+ }
73935
+ function endpoints(edges, symKey, fileKey) {
73936
+ const seen = /* @__PURE__ */ new Map();
73937
+ for (const e of edges) {
73938
+ const symbol = e[symKey];
73939
+ if (!symbol) continue;
73940
+ if (!seen.has(symbol)) seen.set(symbol, { symbol, file: e[fileKey] ?? null });
73941
+ }
73942
+ return [...seen.values()];
73943
+ }
73944
+ function topologyToJson(byQueue) {
73945
+ return [...byQueue.entries()].map(([queueName, edges]) => ({
73946
+ queueName,
73947
+ producers: endpoints(edges, "producerSymbol", "producerFile"),
73948
+ workers: endpoints(edges, "workerSymbol", "workerFile")
73949
+ }));
73950
+ }
73951
+ async function gatherLiveState(config, rows, nameFilter) {
73952
+ let renv;
73953
+ try {
73954
+ renv = resolveEnvironment(config);
73955
+ } catch (err) {
73956
+ return { ok: false, error: err.message };
73957
+ }
73958
+ const queueProvider = queueForEnv(renv);
73959
+ if (!queueProvider) return { ok: false, error: "Redis not configured" };
73960
+ try {
73961
+ const health = await queueProvider.health();
73962
+ if (!health.ok) return { ok: false, error: health.detail };
73963
+ const staticNames = new Set(buildQueueMap(rows).keys());
73964
+ let queueNames;
73965
+ if (nameFilter !== void 0) {
73966
+ queueNames = [nameFilter];
73967
+ } else {
73968
+ const discovered = await queueProvider.discoverQueues().catch(() => []);
73969
+ const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
73970
+ queueNames = union2.size > 0 ? [...union2] : void 0;
73971
+ }
73972
+ const state = await queueProvider.analyzeQueues({ queueNames });
73973
+ return {
73974
+ ok: true,
73975
+ prefix: state.prefix,
73976
+ collectedAt: state.collectedAt,
73977
+ queues: state.queues.map((q) => ({ ...q, runtimeOnly: !staticNames.has(q.queueName) }))
73978
+ };
73979
+ } finally {
73980
+ await queueProvider.close().catch(() => {
73981
+ });
73982
+ }
73983
+ }
73984
+ function printTopology(byQueue) {
73985
+ for (const [queueName, edges] of byQueue) {
73986
+ console.log(import_picocolors4.default.bold(queueName));
73987
+ const producerSet = /* @__PURE__ */ new Set();
73988
+ const producerDetails = /* @__PURE__ */ new Map();
73989
+ for (const edge of edges) {
73990
+ if (edge.producerSymbol) {
73991
+ producerSet.add(edge.producerSymbol);
73992
+ if (edge.producerFile) producerDetails.set(edge.producerSymbol, edge.producerFile);
73993
+ }
73994
+ }
73995
+ if (producerSet.size === 0) {
73996
+ console.log(" producers: " + import_picocolors4.default.dim("none"));
73997
+ } else {
73998
+ const list = Array.from(producerSet).map((sym) => {
73999
+ const file = producerDetails.get(sym);
74000
+ return file ? `${sym} (${file})` : sym;
74001
+ }).join(", ");
74002
+ console.log(" producers: " + list);
74003
+ }
74004
+ const workerSet = /* @__PURE__ */ new Set();
74005
+ const workerDetails = /* @__PURE__ */ new Map();
74006
+ for (const edge of edges) {
74007
+ if (edge.workerSymbol) {
74008
+ workerSet.add(edge.workerSymbol);
74009
+ if (edge.workerFile) workerDetails.set(edge.workerSymbol, edge.workerFile);
74010
+ }
74011
+ }
74012
+ if (workerSet.size === 0) {
74013
+ console.log(" workers: " + import_picocolors4.default.dim("none"));
74014
+ } else {
74015
+ const list = Array.from(workerSet).map((sym) => {
74016
+ const file = workerDetails.get(sym);
74017
+ return file ? `${sym} (${file})` : sym;
74018
+ }).join(", ");
74019
+ console.log(" workers: " + list);
74020
+ }
74021
+ console.log("");
74022
+ }
74023
+ }
74024
+ async function runLiveMode(config, rows, nameFilter, aiOpts) {
74025
+ let renv;
74026
+ try {
74027
+ renv = resolveEnvironment(config);
74028
+ } catch (err) {
74029
+ console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
74030
+ console.log(import_picocolors4.default.yellow(` \u26A0 Cannot resolve environment: ${err.message}`));
74031
+ return;
74032
+ }
74033
+ const queueProvider = queueForEnv(renv);
74034
+ if (!queueProvider) {
74035
+ console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
74036
+ console.log(
74037
+ import_picocolors4.default.yellow(" \u25CB Redis not configured \u2014 run: ") + import_picocolors4.default.bold("horus connect redis")
74038
+ );
74039
+ return;
74040
+ }
74041
+ let headerPrinted = false;
74042
+ try {
74043
+ const health = await queueProvider.health();
74044
+ if (!health.ok) {
74045
+ console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
74046
+ console.log(import_picocolors4.default.red(` \u2717 Redis unreachable: ${health.detail}`));
74047
+ return;
74048
+ }
74049
+ const staticNames = new Set(buildQueueMap(rows).keys());
74050
+ let queueNames;
74051
+ if (nameFilter !== void 0) {
74052
+ queueNames = [nameFilter];
74053
+ } else {
74054
+ const discovered = await queueProvider.discoverQueues().catch(() => []);
74055
+ const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
74056
+ queueNames = union2.size > 0 ? [...union2] : void 0;
74057
+ }
74058
+ const state = await queueProvider.analyzeQueues({ queueNames });
74059
+ const collectedAt = new Date(state.collectedAt).toLocaleTimeString();
74060
+ console.log(
74061
+ import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(` \xB7 source: Redis/BullMQ (prefix: ${state.prefix}) \xB7 collected: ${collectedAt}`)
74062
+ );
74063
+ headerPrinted = true;
74064
+ console.log("");
74065
+ if (state.queues.length === 0) {
74066
+ console.log(import_picocolors4.default.dim(" No queues found in Redis."));
74067
+ console.log(
74068
+ import_picocolors4.default.dim(
74069
+ " If queues exist under a custom prefix, set the BullMQ prefix in your connector config."
74070
+ )
74071
+ );
74072
+ return;
74073
+ }
74074
+ printLiveTable(state.queues, staticNames);
74075
+ if (aiOpts) {
74076
+ const evidence2 = {
74077
+ prefix: state.prefix,
74078
+ collectedAt: state.collectedAt,
74079
+ queues: state.queues.map((q) => ({ ...q, runtimeOnly: !staticNames.has(q.queueName) }))
74080
+ };
74081
+ const result = await renderAiInterpretation({
74082
+ command: "queues",
74083
+ evidence: evidence2,
74084
+ promptKind: "evidence-summary",
74085
+ outputContract: QUEUES_AI_CONTRACT,
74086
+ config: aiOpts.config,
74087
+ modelOverride: aiOpts.aiModel,
74088
+ provider: aiOpts._aiProvider
74089
+ });
74090
+ console.log("\n" + renderInterpretation(result));
74091
+ if (!result.ok) {
74092
+ console.error(import_picocolors4.default.yellow(`[ai] ${result.warning}`));
74093
+ }
74094
+ }
74095
+ } catch (err) {
74096
+ if (!headerPrinted) {
74097
+ console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
74098
+ }
74099
+ console.log(import_picocolors4.default.red(` \u2717 ${err.message}`));
74100
+ } finally {
74101
+ await queueProvider.close().catch(() => {
74102
+ });
74103
+ }
74104
+ }
74105
+ function printLiveTable(queues, staticNames = /* @__PURE__ */ new Set()) {
74106
+ const nameWidth = Math.max(10, ...queues.map((q) => q.queueName.length));
74107
+ const numWidth = 7;
74108
+ const header = " " + "queue".padEnd(nameWidth) + " " + "waiting".padStart(numWidth) + " " + "active".padStart(numWidth) + " " + "failed".padStart(numWidth) + " " + "delayed".padStart(numWidth) + " " + "paused".padStart(numWidth);
74109
+ console.log(import_picocolors4.default.dim(header));
74110
+ console.log(import_picocolors4.default.dim(" " + "\u2500".repeat(header.length - 2)));
74111
+ for (const q of queues) {
74112
+ const hasIssue = q.failed > 0 || q.waiting > 100 || q.delayed > 50 || q.isPaused;
74113
+ const color = hasIssue ? import_picocolors4.default.yellow : (s) => s;
74114
+ 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));
74115
+ console.log(color(row));
74116
+ if (!staticNames.has(q.queueName)) {
74117
+ console.log(import_picocolors4.default.dim(" runtime-only \xB7 no static producer/worker mapping"));
74118
+ }
74119
+ if (q.oldestWaitingMs !== void 0) {
74120
+ const age = formatAge(q.oldestWaitingMs);
74121
+ console.log(import_picocolors4.default.dim(` oldest waiting: ${age}`));
74122
+ }
74123
+ if (q.failedBreakdown && q.failedBreakdown.length > 0) {
74124
+ for (const { reason, count } of q.failedBreakdown.slice(0, 3)) {
74125
+ console.log(import_picocolors4.default.red(` \u2717 [${count}x] ${reason}`));
74126
+ }
74127
+ }
74128
+ }
74129
+ console.log("");
74130
+ }
74131
+ function formatAge(ms) {
74132
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
74133
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
74134
+ if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
74135
+ return `${Math.round(ms / 864e5)}d`;
74136
+ }
74137
+
74138
+ // ../../packages/cli/src/commands/investigate.ts
74139
+ init_cjs_shims();
74140
+ var import_picocolors5 = __toESM(require_picocolors(), 1);
74141
+ function extractEvidenceExcerpt(e) {
73928
74142
  if (!e.payload || typeof e.payload !== "object") return void 0;
73929
74143
  const p = e.payload;
73930
74144
  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);
@@ -74081,7 +74295,9 @@ async function runInvestigate(hint, opts) {
74081
74295
  elasticsearch: !!renv.connectors.elasticsearch?.url,
74082
74296
  grafana: !!renv.connectors.grafana?.url,
74083
74297
  mongodb: !!renv.connectors.mongodb?.url,
74084
- redis: !!renv.connectors.redis?.url
74298
+ redis: !!renv.connectors.redis?.url,
74299
+ // Queue runtime is configured iff a BullMQ provider was built (HOR-205).
74300
+ queue: !!queue
74085
74301
  }
74086
74302
  }
74087
74303
  );
@@ -74146,6 +74362,23 @@ async function runInvestigate(hint, opts) {
74146
74362
  // ../../packages/cli/src/commands/changes.ts
74147
74363
  init_cjs_shims();
74148
74364
  var import_picocolors6 = __toESM(require_picocolors(), 1);
74365
+ var CHANGES_AI_CONTRACT = `Provide a clearly separated AI change-impact review with:
74366
+
74367
+ Highest-risk changes
74368
+ - Files, symbols, or flows most likely to matter for correctness or stability
74369
+ - Explain why based on affected flows and change types (added/removed/modified)
74370
+
74371
+ Review focus
74372
+ - What the reviewer should inspect first
74373
+ - Specific symbols or files worth extra attention
74374
+
74375
+ Testing suggestions
74376
+ - Specific smoke tests or checks based on affected flows
74377
+ - Only reference flows and symbols Horus found
74378
+
74379
+ Confidence / gaps
74380
+ - Where Horus evidence is strong (full flow coverage)
74381
+ - What is missing (e.g. no affected flows found \u2014 no source index, or no matching flows)`;
74149
74382
  async function runChanges(base2, compare, opts) {
74150
74383
  try {
74151
74384
  const config = await loadConfig(opts.config);
@@ -74158,7 +74391,27 @@ async function runChanges(base2, compare, opts) {
74158
74391
  return 1;
74159
74392
  }
74160
74393
  const report = await changeImpact({ base: base2, compare }, { code });
74161
- console.log(opts.json ? changeImpactToJSON(report) : renderChangeImpact(report));
74394
+ if (opts.json) {
74395
+ console.log(changeImpactToJSON(report));
74396
+ } else {
74397
+ console.log(renderChangeImpact(report));
74398
+ if (opts.ai) {
74399
+ const result = await renderAiInterpretation({
74400
+ command: "changes",
74401
+ userIntent: `base: ${base2}, compare: ${compare ?? "HEAD"}`,
74402
+ evidence: report,
74403
+ promptKind: "change-risk",
74404
+ outputContract: CHANGES_AI_CONTRACT,
74405
+ config: opts.config,
74406
+ modelOverride: opts.aiModel,
74407
+ provider: opts._aiProvider
74408
+ });
74409
+ console.log("\n" + renderInterpretation(result));
74410
+ if (!result.ok) {
74411
+ console.error(import_picocolors6.default.yellow(`[ai] ${result.warning}`));
74412
+ }
74413
+ }
74414
+ }
74162
74415
  return 0;
74163
74416
  } catch (err) {
74164
74417
  console.error(import_picocolors6.default.red(err.message));
@@ -74175,6 +74428,22 @@ function resolveTimelineWindow(opts) {
74175
74428
  const since = opts.all ? void 0 : opts.since ?? DEFAULT_SINCE;
74176
74429
  return { since, usingDefault };
74177
74430
  }
74431
+ var TIMELINE_AI_CONTRACT = `Provide a clearly separated AI interpretation section with:
74432
+
74433
+ Evidence used
74434
+ - List the timeline events, commits, and change-impact signals Horus found
74435
+
74436
+ Narrative
74437
+ - Group events into phases (e.g. pre-change, change window, symptom onset, recovery)
74438
+ - Highlight any suspicious ordering (e.g. a change immediately before first error)
74439
+ - Note what happened before vs. after the first symptom if identifiable
74440
+
74441
+ Confidence
74442
+ - High / Medium / Low with a one-line reason
74443
+
74444
+ Gaps / Next checks
74445
+ - Missing evidence dimensions (logs, metrics, traces, etc.)
74446
+ - Exact Horus commands to fill the gaps`;
74178
74447
  async function runTimeline(service, opts) {
74179
74448
  try {
74180
74449
  const config = await loadConfig(opts.config);
@@ -74203,6 +74472,22 @@ async function runTimeline(service, opts) {
74203
74472
  )
74204
74473
  );
74205
74474
  }
74475
+ if (opts.ai) {
74476
+ const result = await renderAiInterpretation({
74477
+ command: "timeline",
74478
+ userIntent: service ? `service: ${service}` : void 0,
74479
+ evidence: t,
74480
+ promptKind: "timeline-narrative",
74481
+ outputContract: TIMELINE_AI_CONTRACT,
74482
+ config: opts.config,
74483
+ modelOverride: opts.aiModel,
74484
+ provider: opts._aiProvider
74485
+ });
74486
+ console.log("\n" + renderInterpretation(result));
74487
+ if (!result.ok) {
74488
+ console.error(import_picocolors7.default.yellow(`[ai] ${result.warning}`));
74489
+ }
74490
+ }
74206
74491
  }
74207
74492
  return 0;
74208
74493
  } catch (err) {
@@ -74215,6 +74500,21 @@ async function runTimeline(service, opts) {
74215
74500
  init_cjs_shims();
74216
74501
  var import_picocolors8 = __toESM(require_picocolors(), 1);
74217
74502
  var DEFAULT_SINCE2 = "7 days ago";
74503
+ var WHAT_CHANGED_AI_CONTRACT = `Provide a clearly separated AI interpretation section with:
74504
+
74505
+ Evidence used
74506
+ - List the concrete changes/files/commits/timestamps Horus found
74507
+
74508
+ Interpretation
74509
+ - Which change(s) look most incident-relevant and why
74510
+ - Which changes are likely noise and why
74511
+ - Any suspicious ordering or coverage gaps
74512
+
74513
+ Confidence
74514
+ - High / Medium / Low with a one-line reason
74515
+
74516
+ Next checks
74517
+ - Exact Horus commands or files to inspect next`;
74218
74518
  async function runWhatChanged(service, opts) {
74219
74519
  try {
74220
74520
  const config = await loadConfig(opts.config);
@@ -74232,6 +74532,22 @@ async function runWhatChanged(service, opts) {
74232
74532
  { code }
74233
74533
  );
74234
74534
  console.log(opts.json ? whatChangedToJSON(r) : renderWhatChanged(r));
74535
+ if (opts.ai && !opts.json) {
74536
+ const result = await renderAiInterpretation({
74537
+ command: "what-changed",
74538
+ userIntent: service ? `service: ${service}` : void 0,
74539
+ evidence: r,
74540
+ promptKind: "change-risk",
74541
+ outputContract: WHAT_CHANGED_AI_CONTRACT,
74542
+ config: opts.config,
74543
+ modelOverride: opts.aiModel,
74544
+ provider: opts._aiProvider
74545
+ });
74546
+ console.log("\n" + renderInterpretation(result));
74547
+ if (!result.ok) {
74548
+ console.error(import_picocolors8.default.yellow(`[ai] ${result.warning}`));
74549
+ }
74550
+ }
74235
74551
  return 0;
74236
74552
  } catch (err) {
74237
74553
  console.error(import_picocolors8.default.red(err.message));
@@ -74242,6 +74558,27 @@ async function runWhatChanged(service, opts) {
74242
74558
  // ../../packages/cli/src/commands/architecture.ts
74243
74559
  init_cjs_shims();
74244
74560
  var import_picocolors9 = __toESM(require_picocolors(), 1);
74561
+ var ARCHITECTURE_AI_CONTRACT = `Provide a clearly separated AI interpretation section with:
74562
+
74563
+ System summary
74564
+ - What the system appears to do based on subsystems, external systems, and key flows Horus found
74565
+ - Major subsystems and their responsibilities
74566
+
74567
+ Critical paths
74568
+ - Important flows and async boundaries found by Horus
74569
+ - Queues, workers, and external API touch points
74570
+
74571
+ Fragility / risk points
74572
+ - Areas where coupling, queues, external systems, or unclear ownership create risk
74573
+ - Refer only to components and boundaries Horus discovered
74574
+
74575
+ How to investigate this system
74576
+ - Useful Horus commands to explore further (blast-radius, investigate, timeline, etc.)
74577
+ - Specific files or symbols worth inspecting
74578
+
74579
+ Confidence / gaps
74580
+ - Where Horus evidence was strong (e.g. indexed source, queue edges)
74581
+ - What evidence is missing and what to do to fill those gaps`;
74245
74582
  async function runArchitecture(opts) {
74246
74583
  try {
74247
74584
  const config = await loadConfig(opts.config);
@@ -74261,7 +74598,26 @@ async function runArchitecture(opts) {
74261
74598
  const { db, sql: sql2 } = createDb(config.database.url);
74262
74599
  try {
74263
74600
  const m = await discoverArchitecture({ code, db, project });
74264
- console.log(opts.json ? architectureToJSON(m) : renderArchitecture(m));
74601
+ if (opts.json) {
74602
+ console.log(architectureToJSON(m));
74603
+ } else {
74604
+ console.log(renderArchitecture(m));
74605
+ if (opts.ai) {
74606
+ const result = await renderAiInterpretation({
74607
+ command: "architecture",
74608
+ evidence: m,
74609
+ promptKind: "system-explanation",
74610
+ outputContract: ARCHITECTURE_AI_CONTRACT,
74611
+ config: opts.config,
74612
+ modelOverride: opts.aiModel,
74613
+ provider: opts._aiProvider
74614
+ });
74615
+ console.log("\n" + renderInterpretation(result));
74616
+ if (!result.ok) {
74617
+ console.error(import_picocolors9.default.yellow(`[ai] ${result.warning}`));
74618
+ }
74619
+ }
74620
+ }
74265
74621
  } finally {
74266
74622
  await sql2.end();
74267
74623
  }
@@ -74275,6 +74631,26 @@ async function runArchitecture(opts) {
74275
74631
  // ../../packages/cli/src/commands/blast-radius.ts
74276
74632
  init_cjs_shims();
74277
74633
  var import_picocolors10 = __toESM(require_picocolors(), 1);
74634
+ var BLAST_RADIUS_AI_CONTRACT = `Provide a clearly separated AI interpretation section with:
74635
+
74636
+ Evidence used
74637
+ - The matched component(s), upstream dependencies, downstream callers, and async boundaries Horus found
74638
+
74639
+ Likely impact
74640
+ - What users or operators would notice if this component fails
74641
+ - Which services/flows are inside the blast radius and at direct risk
74642
+ - Which services/flows appear outside the blast radius
74643
+
74644
+ Containment ideas
74645
+ - Safe mitigations grounded only in the dependencies Horus found
74646
+ - Rollback or disable suggestions only when supported by the evidence (prefix with "verify before doing this")
74647
+ - Async boundaries to isolate (queues, workers, cron jobs, webhooks, external APIs)
74648
+
74649
+ Confidence
74650
+ - High / Medium / Low with a one-line reason
74651
+
74652
+ Next checks
74653
+ - Exact Horus commands or files to inspect next`;
74278
74654
  async function runBlastRadius(query, opts) {
74279
74655
  try {
74280
74656
  const config = await loadConfig(opts.config);
@@ -74302,7 +74678,27 @@ async function runBlastRadius(query, opts) {
74302
74678
  import_picocolors10.default.yellow(` No exact match for "${query}"`) + import_picocolors10.default.dim(` \u2014 showing closest: "${r.seed.name}" (fuzzy match)`)
74303
74679
  );
74304
74680
  }
74305
- console.log(opts.json ? blastRadiusToJSON(r) : renderBlastRadius(r));
74681
+ if (opts.json) {
74682
+ console.log(blastRadiusToJSON(r));
74683
+ } else {
74684
+ console.log(renderBlastRadius(r));
74685
+ if (opts.ai) {
74686
+ const result = await renderAiInterpretation({
74687
+ command: "blast-radius",
74688
+ userIntent: `query: ${query}`,
74689
+ evidence: r,
74690
+ promptKind: "blast-radius",
74691
+ outputContract: BLAST_RADIUS_AI_CONTRACT,
74692
+ config: opts.config,
74693
+ modelOverride: opts.aiModel,
74694
+ provider: opts._aiProvider
74695
+ });
74696
+ console.log("\n" + renderInterpretation(result));
74697
+ if (!result.ok) {
74698
+ console.error(import_picocolors10.default.yellow(`[ai] ${result.warning}`));
74699
+ }
74700
+ }
74701
+ }
74306
74702
  } finally {
74307
74703
  await sql2.end();
74308
74704
  }
@@ -74454,7 +74850,10 @@ async function runReplay(id, opts) {
74454
74850
  console.log(import_picocolors13.default.dim("[ai] Stored judgment replayed. Use --refresh-ai to regenerate."));
74455
74851
  } else {
74456
74852
  const narrativeInput = buildNarrativeInput(report);
74457
- const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
74853
+ const { provider } = await buildNarrativeProvider({
74854
+ config: opts.config,
74855
+ modelOverride: opts.aiModel
74856
+ });
74458
74857
  const { output, fromProvider, degraded, validationErrors } = await renderNarrative(
74459
74858
  narrativeInput,
74460
74859
  { provider }
@@ -74574,7 +74973,10 @@ async function runPostmortem(id, opts) {
74574
74973
  }
74575
74974
  } else {
74576
74975
  const narrativeInput = buildNarrativeInput(report);
74577
- const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
74976
+ const { provider } = await buildNarrativeProvider({
74977
+ config: opts.config,
74978
+ modelOverride: opts.aiModel
74979
+ });
74578
74980
  const { output, fromProvider, degraded, validationErrors } = await renderNarrative(
74579
74981
  narrativeInput,
74580
74982
  { provider }
@@ -74655,6 +75057,19 @@ _AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
74655
75057
  // ../../packages/cli/src/commands/score.ts
74656
75058
  init_cjs_shims();
74657
75059
  var import_picocolors16 = __toESM(require_picocolors(), 1);
75060
+ var SCORE_AI_CONTRACT = `Provide a clearly separated AI score explanation with:
75061
+
75062
+ Why this scored this way
75063
+ - Explain each weak dimension (low value, low weight contribution)
75064
+ - Ground each explanation in what Horus found or failed to find
75065
+
75066
+ Biggest improvement lever
75067
+ - The single dimension where better evidence would move the score most
75068
+ - Exact investigation step or command to gather that evidence
75069
+
75070
+ Suggested improvements
75071
+ - Specific ideas for how Horus could gather better evidence next time
75072
+ - Be concrete: what connector, query, or signal is missing`;
74658
75073
  async function runScore(id, opts) {
74659
75074
  const { db, sql: sql2 } = createDb(await resolveDbUrl(opts.config));
74660
75075
  try {
@@ -74668,7 +75083,27 @@ async function runScore(id, opts) {
74668
75083
  return 1;
74669
75084
  }
74670
75085
  const s = scoreInvestigation(migrateReport(row.report));
74671
- console.log(opts.json ? scoreToJSON(s) : renderScore(s));
75086
+ if (opts.json) {
75087
+ console.log(scoreToJSON(s));
75088
+ } else {
75089
+ console.log(renderScore(s));
75090
+ if (opts.ai) {
75091
+ const result = await renderAiInterpretation({
75092
+ command: "score",
75093
+ userIntent: `investigation: ${id}`,
75094
+ evidence: s,
75095
+ promptKind: "evidence-summary",
75096
+ outputContract: SCORE_AI_CONTRACT,
75097
+ config: opts.config,
75098
+ modelOverride: opts.aiModel,
75099
+ provider: opts._aiProvider
75100
+ });
75101
+ console.log("\n" + renderInterpretation(result));
75102
+ if (!result.ok) {
75103
+ console.error(import_picocolors16.default.yellow(`[ai] ${result.warning}`));
75104
+ }
75105
+ }
75106
+ }
74672
75107
  } finally {
74673
75108
  await sql2.end();
74674
75109
  }
@@ -74755,6 +75190,26 @@ async function runAsk(id, directive, opts) {
74755
75190
  // ../../packages/cli/src/commands/onboard.ts
74756
75191
  init_cjs_shims();
74757
75192
  var import_picocolors18 = __toESM(require_picocolors(), 1);
75193
+ var ONBOARD_AI_CONTRACT = `Provide a clearly separated AI onboarding guide with:
75194
+
75195
+ Start here
75196
+ - Files and components a new engineer should read first (only those Horus found)
75197
+ - Entry points, controllers, workers, or queue producers most relevant to the area
75198
+
75199
+ Mental model
75200
+ - How this area works in plain language, grounded in what Horus discovered
75201
+ - Key data flows and async handoffs
75202
+
75203
+ What breaks here
75204
+ - Common failure modes based on discovered components, async boundaries, and past incidents
75205
+ - Frame each as "if X fails, then Y" using only evidence Horus found
75206
+
75207
+ Useful commands
75208
+ - Exact Horus commands to continue exploring this area
75209
+
75210
+ Confidence / gaps
75211
+ - What Horus knows confidently (indexed source, queue edges, past incidents)
75212
+ - What is missing and would require manual inspection or a broader index`;
74758
75213
  async function runOnboard(area, opts) {
74759
75214
  try {
74760
75215
  const config = await loadConfig(opts.config);
@@ -74780,7 +75235,27 @@ async function runOnboard(area, opts) {
74780
75235
  { area },
74781
75236
  { code, db, repoPath: repo.path, project: renv.project }
74782
75237
  );
74783
- console.log(opts.json ? onboardingToJSON(g) : renderOnboarding(g));
75238
+ if (opts.json) {
75239
+ console.log(onboardingToJSON(g));
75240
+ } else {
75241
+ console.log(renderOnboarding(g));
75242
+ if (opts.ai) {
75243
+ const result = await renderAiInterpretation({
75244
+ command: "onboard",
75245
+ userIntent: area ? `area: ${area}` : void 0,
75246
+ evidence: g,
75247
+ promptKind: "system-explanation",
75248
+ outputContract: ONBOARD_AI_CONTRACT,
75249
+ config: opts.config,
75250
+ modelOverride: opts.aiModel,
75251
+ provider: opts._aiProvider
75252
+ });
75253
+ console.log("\n" + renderInterpretation(result));
75254
+ if (!result.ok) {
75255
+ console.error(import_picocolors18.default.yellow(`[ai] ${result.warning}`));
75256
+ }
75257
+ }
75258
+ }
74784
75259
  } finally {
74785
75260
  await sql2.end();
74786
75261
  }
@@ -74846,6 +75321,24 @@ async function runSimulate(scenarioId, opts) {
74846
75321
  // ../../packages/cli/src/commands/logs.ts
74847
75322
  init_cjs_shims();
74848
75323
  var import_picocolors20 = __toESM(require_picocolors(), 1);
75324
+ var LOGS_AI_CONTRACT = `Provide a clearly separated AI evidence narration with:
75325
+
75326
+ Evidence used
75327
+ - Exact error signatures, counts, first/last timestamps, and affected services Horus found
75328
+
75329
+ What stands out
75330
+ - The most frequent or newest signatures
75331
+ - Services appearing disproportionately across signatures
75332
+
75333
+ What this may indicate
75334
+ - Use "may suggest", "is consistent with", or "could indicate" \u2014 not "proves" or "caused by"
75335
+ - Correlation hints only (e.g. "the spike overlaps with the deployment window")
75336
+
75337
+ What is not proven
75338
+ - Claims that cannot be made from error logs alone (root cause, user impact, fix)
75339
+
75340
+ Next checks
75341
+ - Exact Horus commands, dashboards, or files to inspect next`;
74849
75342
  function sinceToIso(since) {
74850
75343
  if (since === void 0) return void 0;
74851
75344
  const match = /^(\d+)(m|h|d)$/.exec(since);
@@ -75050,6 +75543,27 @@ async function runLogs(service, opts) {
75050
75543
  }
75051
75544
  console.log("");
75052
75545
  console.log(import_picocolors20.default.dim(" (use --raw to see individual log lines)"));
75546
+ if (opts.ai) {
75547
+ const result = await renderAiInterpretation({
75548
+ command: "logs",
75549
+ userIntent: scopeService ? `service: ${scopeService}` : void 0,
75550
+ evidence: {
75551
+ totalErrors: analysis.totalErrors,
75552
+ signatures: analysis.signatures,
75553
+ newSignatures: analysis.newSignatures,
75554
+ affectedServices: analysis.affectedServices
75555
+ },
75556
+ promptKind: "evidence-summary",
75557
+ outputContract: LOGS_AI_CONTRACT,
75558
+ config: opts.config,
75559
+ modelOverride: opts.aiModel,
75560
+ provider: opts._aiProvider
75561
+ });
75562
+ console.log("\n" + renderInterpretation(result));
75563
+ if (!result.ok) {
75564
+ console.error(import_picocolors20.default.yellow(`[ai] ${result.warning}`));
75565
+ }
75566
+ }
75053
75567
  return 0;
75054
75568
  } catch (err) {
75055
75569
  console.error(import_picocolors20.default.red(err.message));
@@ -75060,6 +75574,24 @@ async function runLogs(service, opts) {
75060
75574
  // ../../packages/cli/src/commands/metrics.ts
75061
75575
  init_cjs_shims();
75062
75576
  var import_picocolors21 = __toESM(require_picocolors(), 1);
75577
+ var METRICS_AI_CONTRACT = `Provide a clearly separated AI evidence narration with:
75578
+
75579
+ Evidence used
75580
+ - Exact panel names, anomaly types, baseline vs. current values, and ratios Horus found
75581
+
75582
+ What stands out
75583
+ - The most severe or highest-ratio anomalies
75584
+ - Any metric dimensions with correlated movement (e.g. error rate + latency both spiking)
75585
+
75586
+ What this may indicate
75587
+ - Use "may suggest", "is consistent with", or "could indicate" \u2014 never "proves"
75588
+ - Do not invent dashboard panels or panel names not in the evidence
75589
+
75590
+ What is not proven
75591
+ - Claims that cannot be made from metric anomalies alone (root cause, data loss, user scope)
75592
+
75593
+ Next checks
75594
+ - Exact Horus commands or Grafana dashboards to inspect next`;
75063
75595
  function nowSecs() {
75064
75596
  return Math.floor(Date.now() / 1e3);
75065
75597
  }
@@ -75222,6 +75754,22 @@ async function runMetrics(hint, opts) {
75222
75754
  console.log("");
75223
75755
  console.log(import_picocolors21.default.dim(` ${ok.length} panel series with no anomaly.`));
75224
75756
  }
75757
+ if (opts.ai) {
75758
+ const result = await renderAiInterpretation({
75759
+ command: "metrics",
75760
+ userIntent: hint ? `hint: ${hint}` : void 0,
75761
+ evidence: findings2,
75762
+ promptKind: "evidence-summary",
75763
+ outputContract: METRICS_AI_CONTRACT,
75764
+ config: opts.config,
75765
+ modelOverride: opts.aiModel,
75766
+ provider: opts._aiProvider
75767
+ });
75768
+ console.log("\n" + renderInterpretation(result));
75769
+ if (!result.ok) {
75770
+ console.error(import_picocolors21.default.yellow(`[ai] ${result.warning}`));
75771
+ }
75772
+ }
75225
75773
  return 0;
75226
75774
  } catch (err) {
75227
75775
  console.error(import_picocolors21.default.red(err.message));
@@ -75232,6 +75780,24 @@ async function runMetrics(hint, opts) {
75232
75780
  // ../../packages/cli/src/commands/state.ts
75233
75781
  init_cjs_shims();
75234
75782
  var import_picocolors22 = __toESM(require_picocolors(), 1);
75783
+ var STATE_AI_CONTRACT = `Provide a clearly separated AI evidence narration with:
75784
+
75785
+ Evidence used
75786
+ - Exact collection names, document counts, staleness hours, and anomalous status values Horus found
75787
+
75788
+ What stands out
75789
+ - Collections with stale records or anomalous status distributions
75790
+ - Any counts that seem unusually low or high
75791
+
75792
+ What this may indicate
75793
+ - Use "may suggest", "is consistent with", or "could indicate" \u2014 never "proves"
75794
+ - Do not infer private data or claim access to document contents
75795
+
75796
+ What is not proven
75797
+ - Claims that require reading actual document content or non-allowlisted collections
75798
+
75799
+ Next checks
75800
+ - Exact Horus commands or database queries to inspect next`;
75235
75801
  async function runState(opts) {
75236
75802
  try {
75237
75803
  const config = await loadConfig(opts.config, { name: opts.name });
@@ -75316,6 +75882,21 @@ async function runState(opts) {
75316
75882
  );
75317
75883
  }
75318
75884
  }
75885
+ if (opts.ai) {
75886
+ const result = await renderAiInterpretation({
75887
+ command: "state",
75888
+ evidence: analysis,
75889
+ promptKind: "evidence-summary",
75890
+ outputContract: STATE_AI_CONTRACT,
75891
+ config: opts.config,
75892
+ modelOverride: opts.aiModel,
75893
+ provider: opts._aiProvider
75894
+ });
75895
+ console.log("\n" + renderInterpretation(result));
75896
+ if (!result.ok) {
75897
+ console.error(import_picocolors22.default.yellow(`[ai] ${result.warning}`));
75898
+ }
75899
+ }
75319
75900
  return 0;
75320
75901
  } finally {
75321
75902
  await mongo.close();
@@ -77083,6 +77664,26 @@ async function runGenerateConfig(opts) {
77083
77664
  // ../../packages/cli/src/commands/readiness.ts
77084
77665
  init_cjs_shims();
77085
77666
  var import_picocolors34 = __toESM(require_picocolors(), 1);
77667
+ var READINESS_AI_CONTRACT = `Provide a clearly separated AI readiness summary with:
77668
+
77669
+ Overall assessment
77670
+ - Release/demo readiness in plain language (Ready / Partially ready / Not ready)
77671
+ - One sentence explaining why
77672
+
77673
+ Blockers
77674
+ - Each failing blocking check and what it means operationally
77675
+ - Only reference checks that Horus found failing
77676
+
77677
+ Risks
77678
+ - Non-blocking warnings that could limit investigation depth or demo quality
77679
+ - Prioritize by likely impact
77680
+
77681
+ Recommended next action
77682
+ - The single most impactful step to take right now
77683
+ - Exact command or config change based on the checks above
77684
+
77685
+ Confidence / gaps
77686
+ - Which checks Horus could verify vs. which require manual confirmation`;
77086
77687
  var DEFAULT_DB_URL5 = "postgresql://horus:horus@localhost:5433/horus";
77087
77688
  function mark3(status) {
77088
77689
  if (status === "pass") return import_picocolors34.default.green("\u2713");
@@ -77280,6 +77881,21 @@ async function runReadiness(opts) {
77280
77881
  write(import_picocolors34.default.dim(" Re-run `horus readiness` after resolving the items above."));
77281
77882
  }
77282
77883
  write("");
77884
+ if (opts?.ai) {
77885
+ const result = await renderAiInterpretation({
77886
+ command: "readiness",
77887
+ evidence: checks,
77888
+ promptKind: "readiness",
77889
+ outputContract: READINESS_AI_CONTRACT,
77890
+ config: opts.config,
77891
+ modelOverride: opts.aiModel,
77892
+ provider: opts._aiProvider
77893
+ });
77894
+ console.log("\n" + renderInterpretation(result));
77895
+ if (!result.ok) {
77896
+ console.error(import_picocolors34.default.yellow(`[ai] ${result.warning}`));
77897
+ }
77898
+ }
77283
77899
  return blockingFails.length > 0 ? 1 : 0;
77284
77900
  }
77285
77901
 
@@ -77323,8 +77939,8 @@ Examples:
77323
77939
  program2.command("generate-config").description("Create a starter horus.config.js with placeholders (HOR-90)").option("--out <path>", "output path (default: horus.config.js in cwd)").option("--name <name>", "project name placeholder (default: my-project)").option("--repo <path>", "repository path placeholder (default: /path/to/<name>)").option("--force", "overwrite an existing config file").action(async (opts) => {
77324
77940
  process.exitCode = await runGenerateConfig(opts);
77325
77941
  });
77326
- program2.command("readiness").description("Summarize release/demo readiness: DB, source intelligence, connectors, and local config (HOR-97)").option("-c, --config <path>", "path to horus.config.js").action(async (opts) => {
77327
- process.exitCode = await runReadiness({ config: opts.config });
77942
+ program2.command("readiness").description("Summarize release/demo readiness: DB, source intelligence, connectors, and local config (HOR-97)").option("-c, --config <path>", "path to horus.config.js").option("--ai", "append AI readiness summary and recommended next action").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(async (opts) => {
77943
+ process.exitCode = await runReadiness({ config: opts.config, ai: opts.ai, aiModel: opts.aiModel });
77328
77944
  });
77329
77945
  program2.command("connect <type>").description(
77330
77946
  "Add or update a connector (elasticsearch / mongodb / grafana / redis / ai) in .horus/config.json"
@@ -77391,14 +78007,16 @@ Examples:
77391
78007
  process.exitCode = await runIndex(opts);
77392
78008
  }
77393
78009
  );
77394
- 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").option("--json", "output JSON").action(
78010
+ 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").option("--json", "output JSON").option("--ai", "append AI narration of queue topology and live state (most useful with --live)").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(
77395
78011
  async (name, opts) => {
77396
78012
  process.exitCode = await runQueues(name, {
77397
78013
  config: opts.config,
77398
78014
  name: opts.name,
77399
78015
  project: opts.project,
77400
78016
  live: opts.live,
77401
- json: opts.json
78017
+ json: opts.json,
78018
+ ai: opts.ai,
78019
+ aiModel: opts.aiModel
77402
78020
  });
77403
78021
  }
77404
78022
  );
@@ -77430,12 +78048,12 @@ Examples:
77430
78048
  `);
77431
78049
  program2.command("changes <base> [compare]").description(
77432
78050
  "Show what changed between two git refs and which flows are affected (source change-impact)"
77433
- ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").action(async (base2, compare, opts) => {
77434
- process.exitCode = await runChanges(base2, compare, { config: opts.config, json: opts.json });
78051
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").option("--ai", "append AI change-impact review").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(async (base2, compare, opts) => {
78052
+ process.exitCode = await runChanges(base2, compare, { config: opts.config, json: opts.json, ai: opts.ai, aiModel: opts.aiModel });
77435
78053
  });
77436
78054
  program2.command("timeline [service]").description(
77437
78055
  "Reconstruct what changed in a time window (git + change-impact) \u2014 evidence, not conclusions"
77438
- ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (default "7 days ago"; e.g. "30 days ago", a date)').option("--until <when>", "git --until").option("--all", "include all history instead of the default recent window").option("--json", "output JSON").action(
78056
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (default "7 days ago"; e.g. "30 days ago", a date)').option("--until <when>", "git --until").option("--all", "include all history instead of the default recent window").option("--json", "output JSON").option("--ai", "append AI narrative interpretation of the timeline").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(
77439
78057
  async (service, opts) => {
77440
78058
  process.exitCode = await runTimeline(service, {
77441
78059
  config: opts.config,
@@ -77443,36 +78061,42 @@ Examples:
77443
78061
  since: opts.since,
77444
78062
  until: opts.until,
77445
78063
  all: opts.all,
77446
- json: opts.json
78064
+ json: opts.json,
78065
+ ai: opts.ai,
78066
+ aiModel: opts.aiModel
77447
78067
  });
77448
78068
  }
77449
78069
  );
77450
78070
  program2.command("what-changed [service]").description(
77451
78071
  "Concise, evidence-backed summary of what changed for a service in a time window"
77452
- ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (default "7 days ago")').option("--until <when>", "git --until").option("--json", "output JSON").action(
78072
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (default "7 days ago")').option("--until <when>", "git --until").option("--json", "output JSON").option("--ai", "append AI interpretation of the changes").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(
77453
78073
  async (service, opts) => {
77454
78074
  process.exitCode = await runWhatChanged(service, {
77455
78075
  config: opts.config,
77456
78076
  repo: opts.repo,
77457
78077
  since: opts.since,
77458
78078
  until: opts.until,
77459
- json: opts.json
78079
+ json: opts.json,
78080
+ ai: opts.ai,
78081
+ aiModel: opts.aiModel
77460
78082
  });
77461
78083
  }
77462
78084
  );
77463
78085
  program2.command("architecture").description(
77464
78086
  "Discover the living architecture (subsystems, async boundaries, external systems, fragility)"
77465
- ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").action(async (opts) => {
77466
- process.exitCode = await runArchitecture({ config: opts.config, json: opts.json });
78087
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").option("--ai", "append AI system explanation grounded in discovered architecture").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(async (opts) => {
78088
+ process.exitCode = await runArchitecture({ config: opts.config, json: opts.json, ai: opts.ai, aiModel: opts.aiModel });
77467
78089
  });
77468
78090
  program2.command("blast-radius <query>").description(
77469
78091
  "Failure-propagation analysis: upstream/downstream dependencies + blast radius across async boundaries"
77470
- ).option("-c, --config <path>", "path to horus.config.ts").option("-d, --depth <n>", "traversal depth", (v) => parseInt(v, 10)).option("--json", "output JSON").action(
78092
+ ).option("-c, --config <path>", "path to horus.config.ts").option("-d, --depth <n>", "traversal depth", (v) => parseInt(v, 10)).option("--json", "output JSON").option("--ai", "append AI severity and containment interpretation").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(
77471
78093
  async (query, opts) => {
77472
78094
  process.exitCode = await runBlastRadius(query, {
77473
78095
  config: opts.config,
77474
78096
  depth: opts.depth,
77475
- json: opts.json
78097
+ json: opts.json,
78098
+ ai: opts.ai,
78099
+ aiModel: opts.aiModel
77476
78100
  });
77477
78101
  }
77478
78102
  );
@@ -77536,8 +78160,8 @@ Examples:
77536
78160
  );
77537
78161
  program2.command("score <id>").description(
77538
78162
  "Score a saved investigation's quality (a feedback loop for Horus, not engineers)"
77539
- ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").action(async (id, opts) => {
77540
- process.exitCode = await runScore(id, { config: opts.config, json: opts.json });
78163
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").option("--ai", "append AI score explanation and improvement suggestions").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(async (id, opts) => {
78164
+ process.exitCode = await runScore(id, { config: opts.config, json: opts.json, ai: opts.ai, aiModel: opts.aiModel });
77541
78165
  });
77542
78166
  program2.command("ask <id> <directive>").description(
77543
78167
  'Ask about or refine a saved investigation \u2014 reuses evidence, no re-query.\n Questions (direct answers):\n "what evidence contradicts <topic>?" \xB7 "what evidence is missing?"\n "why is confidence not higher?"\n Topic filters (deterministic scoping):\n "focus on queue behavior" \xB7 "ignore deployment changes" \xB7 "retry"'
@@ -77549,11 +78173,13 @@ Examples:
77549
78173
  });
77550
78174
  program2.command("onboard [area]").description(
77551
78175
  "Understand a system fast: architecture, critical paths, what breaks, ownership, past incidents"
77552
- ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--json", "output JSON").action(async (area, opts) => {
78176
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--json", "output JSON").option("--ai", "append AI onboarding guide grounded in discovered system evidence").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(async (area, opts) => {
77553
78177
  process.exitCode = await runOnboard(area, {
77554
78178
  config: opts.config,
77555
78179
  repo: opts.repo,
77556
- json: opts.json
78180
+ json: opts.json,
78181
+ ai: opts.ai,
78182
+ aiModel: opts.aiModel
77557
78183
  });
77558
78184
  });
77559
78185
  program2.command("simulate [scenario]").description(
@@ -77566,21 +78192,21 @@ Examples:
77566
78192
  });
77567
78193
  program2.command("logs [service]").description(
77568
78194
  "Synthesize error evidence from logs (signatures, first/last, affected services); --raw for lines"
77569
- ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name").option("--env <name>", "environment name (e.g. production)").option("--since <when>", "time window, e.g. 24h, 7d, or an ISO date").option("--level <level>", "minimum level (with --raw): trace|debug|info|warn|error|fatal").option("--grep <text>", "match text in the message").option("--raw", "dump individual log lines instead of synthesized evidence (error+ by default)").option("--all-levels", "with --raw: show all severity levels, not just error+").option("--limit <n>", "max records (with --raw)").option("--json", "output JSON").action(
78195
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name").option("--env <name>", "environment name (e.g. production)").option("--since <when>", "time window, e.g. 24h, 7d, or an ISO date").option("--level <level>", "minimum level (with --raw): trace|debug|info|warn|error|fatal").option("--grep <text>", "match text in the message").option("--raw", "dump individual log lines instead of synthesized evidence (error+ by default)").option("--all-levels", "with --raw: show all severity levels, not just error+").option("--limit <n>", "max records (with --raw)").option("--json", "output JSON").option("--ai", "append AI narration of error signatures (default mode only, not --raw or --json)").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(
77570
78196
  async (service, opts) => {
77571
78197
  process.exitCode = await runLogs(service, opts);
77572
78198
  }
77573
78199
  );
77574
78200
  program2.command("state").description(
77575
78201
  "Surface application-state evidence from MongoDB (read-only, allowlisted): counts, staleness, anomalous statuses"
77576
- ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name").option("--env <name>", "environment name (e.g. production)").option("--stale-hours <n>", "staleness threshold in hours (default 24)").option("--json", "output JSON").action(
78202
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name").option("--env <name>", "environment name (e.g. production)").option("--stale-hours <n>", "staleness threshold in hours (default 24)").option("--json", "output JSON").option("--ai", "append AI narration of MongoDB state evidence").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(
77577
78203
  async (opts) => {
77578
78204
  process.exitCode = await runState(opts);
77579
78205
  }
77580
78206
  );
77581
78207
  program2.command("metrics [hint]").description(
77582
78208
  "Grafana metrics evidence: find dashboards/panels for a hint and detect latency spikes, error-rate changes, throughput drops, queue growth"
77583
- ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--since <when>", "window, e.g. 1h, 6h, 24h").option("--step <secs>", "range step seconds").option("--dashboard <uid>", "restrict to a dashboard uid").option("--query <promql>", "raw datasource query escape hatch").option("--json", "JSON output").action(
78209
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--since <when>", "window, e.g. 1h, 6h, 24h").option("--step <secs>", "range step seconds").option("--dashboard <uid>", "restrict to a dashboard uid").option("--query <promql>", "raw datasource query escape hatch").option("--json", "JSON output").option("--ai", "append AI narration of metric findings (default mode only, not --query or --json)").option("--ai-model <model>", "override the AI model (default: claude-opus-4-8)").action(
77584
78210
  async (hint, opts) => {
77585
78211
  process.exitCode = await runMetrics(hint, opts);
77586
78212
  }