@merittdev/horus 0.1.15 → 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 +903 -306
  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.15" : "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
  }
@@ -74428,29 +74824,6 @@ async function runInvestigations(opts) {
74428
74824
  // ../../packages/cli/src/commands/replay.ts
74429
74825
  init_cjs_shims();
74430
74826
  var import_picocolors13 = __toESM(require_picocolors(), 1);
74431
-
74432
- // ../../packages/cli/src/lib/ai-provider.ts
74433
- init_cjs_shims();
74434
- var DEFAULT_MODEL = "claude-opus-4-8";
74435
- async function buildNarrativeProvider(opts) {
74436
- let apiKey;
74437
- let savedModel;
74438
- try {
74439
- const config = await loadConfig(opts.config);
74440
- const ai = resolveAiSettings(config);
74441
- apiKey = ai.anthropicApiKey;
74442
- savedModel = ai.model;
74443
- } catch {
74444
- }
74445
- const model = opts.modelOverride ?? savedModel ?? DEFAULT_MODEL;
74446
- const provider = new AnthropicNarrativeProvider({
74447
- model,
74448
- ...apiKey !== void 0 ? { apiKey } : {}
74449
- });
74450
- return { provider, model };
74451
- }
74452
-
74453
- // ../../packages/cli/src/commands/replay.ts
74454
74827
  async function runReplay(id, opts) {
74455
74828
  const { db, sql: sql2 } = createDb(await resolveDbUrl(opts.config));
74456
74829
  try {
@@ -74684,6 +75057,19 @@ _AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
74684
75057
  // ../../packages/cli/src/commands/score.ts
74685
75058
  init_cjs_shims();
74686
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`;
74687
75073
  async function runScore(id, opts) {
74688
75074
  const { db, sql: sql2 } = createDb(await resolveDbUrl(opts.config));
74689
75075
  try {
@@ -74697,7 +75083,27 @@ async function runScore(id, opts) {
74697
75083
  return 1;
74698
75084
  }
74699
75085
  const s = scoreInvestigation(migrateReport(row.report));
74700
- 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
+ }
74701
75107
  } finally {
74702
75108
  await sql2.end();
74703
75109
  }
@@ -74784,6 +75190,26 @@ async function runAsk(id, directive, opts) {
74784
75190
  // ../../packages/cli/src/commands/onboard.ts
74785
75191
  init_cjs_shims();
74786
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`;
74787
75213
  async function runOnboard(area, opts) {
74788
75214
  try {
74789
75215
  const config = await loadConfig(opts.config);
@@ -74809,7 +75235,27 @@ async function runOnboard(area, opts) {
74809
75235
  { area },
74810
75236
  { code, db, repoPath: repo.path, project: renv.project }
74811
75237
  );
74812
- 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
+ }
74813
75259
  } finally {
74814
75260
  await sql2.end();
74815
75261
  }
@@ -74875,6 +75321,24 @@ async function runSimulate(scenarioId, opts) {
74875
75321
  // ../../packages/cli/src/commands/logs.ts
74876
75322
  init_cjs_shims();
74877
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`;
74878
75342
  function sinceToIso(since) {
74879
75343
  if (since === void 0) return void 0;
74880
75344
  const match = /^(\d+)(m|h|d)$/.exec(since);
@@ -75079,6 +75543,27 @@ async function runLogs(service, opts) {
75079
75543
  }
75080
75544
  console.log("");
75081
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
+ }
75082
75567
  return 0;
75083
75568
  } catch (err) {
75084
75569
  console.error(import_picocolors20.default.red(err.message));
@@ -75089,6 +75574,24 @@ async function runLogs(service, opts) {
75089
75574
  // ../../packages/cli/src/commands/metrics.ts
75090
75575
  init_cjs_shims();
75091
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`;
75092
75595
  function nowSecs() {
75093
75596
  return Math.floor(Date.now() / 1e3);
75094
75597
  }
@@ -75251,6 +75754,22 @@ async function runMetrics(hint, opts) {
75251
75754
  console.log("");
75252
75755
  console.log(import_picocolors21.default.dim(` ${ok.length} panel series with no anomaly.`));
75253
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
+ }
75254
75773
  return 0;
75255
75774
  } catch (err) {
75256
75775
  console.error(import_picocolors21.default.red(err.message));
@@ -75261,6 +75780,24 @@ async function runMetrics(hint, opts) {
75261
75780
  // ../../packages/cli/src/commands/state.ts
75262
75781
  init_cjs_shims();
75263
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`;
75264
75801
  async function runState(opts) {
75265
75802
  try {
75266
75803
  const config = await loadConfig(opts.config, { name: opts.name });
@@ -75345,6 +75882,21 @@ async function runState(opts) {
75345
75882
  );
75346
75883
  }
75347
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
+ }
75348
75900
  return 0;
75349
75901
  } finally {
75350
75902
  await mongo.close();
@@ -77112,6 +77664,26 @@ async function runGenerateConfig(opts) {
77112
77664
  // ../../packages/cli/src/commands/readiness.ts
77113
77665
  init_cjs_shims();
77114
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`;
77115
77687
  var DEFAULT_DB_URL5 = "postgresql://horus:horus@localhost:5433/horus";
77116
77688
  function mark3(status) {
77117
77689
  if (status === "pass") return import_picocolors34.default.green("\u2713");
@@ -77309,6 +77881,21 @@ async function runReadiness(opts) {
77309
77881
  write(import_picocolors34.default.dim(" Re-run `horus readiness` after resolving the items above."));
77310
77882
  }
77311
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
+ }
77312
77899
  return blockingFails.length > 0 ? 1 : 0;
77313
77900
  }
77314
77901
 
@@ -77352,8 +77939,8 @@ Examples:
77352
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) => {
77353
77940
  process.exitCode = await runGenerateConfig(opts);
77354
77941
  });
77355
- 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) => {
77356
- 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 });
77357
77944
  });
77358
77945
  program2.command("connect <type>").description(
77359
77946
  "Add or update a connector (elasticsearch / mongodb / grafana / redis / ai) in .horus/config.json"
@@ -77420,14 +78007,16 @@ Examples:
77420
78007
  process.exitCode = await runIndex(opts);
77421
78008
  }
77422
78009
  );
77423
- 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(
77424
78011
  async (name, opts) => {
77425
78012
  process.exitCode = await runQueues(name, {
77426
78013
  config: opts.config,
77427
78014
  name: opts.name,
77428
78015
  project: opts.project,
77429
78016
  live: opts.live,
77430
- json: opts.json
78017
+ json: opts.json,
78018
+ ai: opts.ai,
78019
+ aiModel: opts.aiModel
77431
78020
  });
77432
78021
  }
77433
78022
  );
@@ -77459,12 +78048,12 @@ Examples:
77459
78048
  `);
77460
78049
  program2.command("changes <base> [compare]").description(
77461
78050
  "Show what changed between two git refs and which flows are affected (source change-impact)"
77462
- ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").action(async (base2, compare, opts) => {
77463
- 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 });
77464
78053
  });
77465
78054
  program2.command("timeline [service]").description(
77466
78055
  "Reconstruct what changed in a time window (git + change-impact) \u2014 evidence, not conclusions"
77467
- ).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(
77468
78057
  async (service, opts) => {
77469
78058
  process.exitCode = await runTimeline(service, {
77470
78059
  config: opts.config,
@@ -77472,36 +78061,42 @@ Examples:
77472
78061
  since: opts.since,
77473
78062
  until: opts.until,
77474
78063
  all: opts.all,
77475
- json: opts.json
78064
+ json: opts.json,
78065
+ ai: opts.ai,
78066
+ aiModel: opts.aiModel
77476
78067
  });
77477
78068
  }
77478
78069
  );
77479
78070
  program2.command("what-changed [service]").description(
77480
78071
  "Concise, evidence-backed summary of what changed for a service in a time window"
77481
- ).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(
77482
78073
  async (service, opts) => {
77483
78074
  process.exitCode = await runWhatChanged(service, {
77484
78075
  config: opts.config,
77485
78076
  repo: opts.repo,
77486
78077
  since: opts.since,
77487
78078
  until: opts.until,
77488
- json: opts.json
78079
+ json: opts.json,
78080
+ ai: opts.ai,
78081
+ aiModel: opts.aiModel
77489
78082
  });
77490
78083
  }
77491
78084
  );
77492
78085
  program2.command("architecture").description(
77493
78086
  "Discover the living architecture (subsystems, async boundaries, external systems, fragility)"
77494
- ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").action(async (opts) => {
77495
- 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 });
77496
78089
  });
77497
78090
  program2.command("blast-radius <query>").description(
77498
78091
  "Failure-propagation analysis: upstream/downstream dependencies + blast radius across async boundaries"
77499
- ).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(
77500
78093
  async (query, opts) => {
77501
78094
  process.exitCode = await runBlastRadius(query, {
77502
78095
  config: opts.config,
77503
78096
  depth: opts.depth,
77504
- json: opts.json
78097
+ json: opts.json,
78098
+ ai: opts.ai,
78099
+ aiModel: opts.aiModel
77505
78100
  });
77506
78101
  }
77507
78102
  );
@@ -77565,8 +78160,8 @@ Examples:
77565
78160
  );
77566
78161
  program2.command("score <id>").description(
77567
78162
  "Score a saved investigation's quality (a feedback loop for Horus, not engineers)"
77568
- ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").action(async (id, opts) => {
77569
- 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 });
77570
78165
  });
77571
78166
  program2.command("ask <id> <directive>").description(
77572
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"'
@@ -77578,11 +78173,13 @@ Examples:
77578
78173
  });
77579
78174
  program2.command("onboard [area]").description(
77580
78175
  "Understand a system fast: architecture, critical paths, what breaks, ownership, past incidents"
77581
- ).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) => {
77582
78177
  process.exitCode = await runOnboard(area, {
77583
78178
  config: opts.config,
77584
78179
  repo: opts.repo,
77585
- json: opts.json
78180
+ json: opts.json,
78181
+ ai: opts.ai,
78182
+ aiModel: opts.aiModel
77586
78183
  });
77587
78184
  });
77588
78185
  program2.command("simulate [scenario]").description(
@@ -77595,21 +78192,21 @@ Examples:
77595
78192
  });
77596
78193
  program2.command("logs [service]").description(
77597
78194
  "Synthesize error evidence from logs (signatures, first/last, affected services); --raw for lines"
77598
- ).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(
77599
78196
  async (service, opts) => {
77600
78197
  process.exitCode = await runLogs(service, opts);
77601
78198
  }
77602
78199
  );
77603
78200
  program2.command("state").description(
77604
78201
  "Surface application-state evidence from MongoDB (read-only, allowlisted): counts, staleness, anomalous statuses"
77605
- ).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(
77606
78203
  async (opts) => {
77607
78204
  process.exitCode = await runState(opts);
77608
78205
  }
77609
78206
  );
77610
78207
  program2.command("metrics [hint]").description(
77611
78208
  "Grafana metrics evidence: find dashboards/panels for a hint and detect latency spikes, error-rate changes, throughput drops, queue growth"
77612
- ).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(
77613
78210
  async (hint, opts) => {
77614
78211
  process.exitCode = await runMetrics(hint, opts);
77615
78212
  }