@merittdev/horus 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +355 -0
  2. package/dist/index.cjs +118 -28
  3. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,355 @@
1
+ <p align="center">
2
+ <img src="https://meritt-dev-assets.s3.eu-central-1.amazonaws.com/public/horus-logo-dark-20260614171147.svg" width="72" alt="Horus" />
3
+ </p>
4
+
5
+ # Horus
6
+
7
+ **Understand what happened.**
8
+
9
+ Open-source incident investigation. Horus connects Elasticsearch, Grafana, MongoDB, BullMQ, and source intelligence into deterministic reports — installable today.
10
+
11
+ CLI-only. Read-only against production systems. Horus never writes to your infrastructure.
12
+
13
+ **Website:** [horus.sh](https://horus.sh) · **Source:** [github.com/meritt-dev/horus](https://github.com/meritt-dev/horus)
14
+
15
+ ```bash
16
+ curl -fsSL https://horus.sh/install.sh | bash
17
+ npm install -g @merittdev/horus
18
+ brew install meritt-dev/tap/horus
19
+ ```
20
+
21
+ Homebrew tap is live through meritt-dev/tap.
22
+
23
+ ---
24
+
25
+ ## What Horus does
26
+
27
+ Horus reads from your existing systems and reconstructs the incident through evidence, correlation, and ranked hypotheses.
28
+
29
+ It does not dump thousands of logs. It connects runtime signals to source context and returns a **deterministic report** — suspected causes (ranked), hypotheses, evidence, gaps, and next actions. Evidence before inference. Optional `--ai` adds an Anthropic narrative on top.
30
+
31
+ Every incident leaves evidence.
32
+
33
+ ## What Horus is not
34
+
35
+ | | |
36
+ |---|---|
37
+ | **Monitoring** | Detects problems |
38
+ | **Observability** | Shows signals |
39
+ | **Horus** | Reconstructs what happened |
40
+
41
+ Horus is not another dashboard, alerting tool, or log viewer. It sits on top of the systems you already use.
42
+
43
+ > Monitoring detects. Observability shows. Horus reconstructs.
44
+
45
+ ## Getting started
46
+
47
+ ```bash
48
+ horus setup
49
+ horus init
50
+ horus index
51
+ horus connect elasticsearch # optional runtime connectors
52
+ horus investigate "checkout latency spike"
53
+ horus investigations # list saved IDs
54
+ horus replay <id>
55
+ horus postmortem <id>
56
+ ```
57
+
58
+ Horus source intelligence requires a local code-graph host (the curl installer attempts to install it). Postgres is required for the audit store.
59
+
60
+ ## How it works
61
+
62
+ **Evidence in. Explanation out.**
63
+
64
+ | Runtime + Source | Investigation Engine | Investigation Report |
65
+ |---|---|---|
66
+ | Elasticsearch logs | Correlation | Suspected causes (ranked) |
67
+ | Grafana metrics | Timeline | Hypotheses + confidence |
68
+ | MongoDB state | Cause ranking | Evidence + gaps |
69
+ | BullMQ queues | | Next actions |
70
+ | Source graph + git | | |
71
+
72
+ Pipeline: **Evidence → Correlation → Hypotheses → Timeline → Report**
73
+
74
+ ## Sources Horus investigates
75
+
76
+ Elasticsearch · Grafana · MongoDB · BullMQ · Git changes · Source graph · Queue map · Ownership
77
+
78
+ Trace reconstruction is not shipped yet. Connectors are read-only and project-scoped.
79
+
80
+ ## Example output (illustrative)
81
+
82
+ ```bash
83
+ horus investigate \
84
+ --project atlas-payments \
85
+ --env production \
86
+ "checkout latency spike"
87
+ ```
88
+
89
+ ```text
90
+ # Investigation inv-347
91
+ Hint: checkout latency spike
92
+
93
+ ## Suspected causes (ranked)
94
+ 1. [0.82 / high] Redis connection pool exhaustion [↑ queue]
95
+
96
+ ## Hypotheses
97
+ [supported] [0.78] queue: backlog growth preceded latency spike
98
+
99
+ ## Evidence gaps
100
+ - queue runtime state: worker heartbeat unavailable
101
+
102
+ ## Evidence
103
+ - ev-01 [elasticsearch/error] Request timeout increase
104
+ - ev-04 [bullmq/queue] checkout-jobs backlog growth
105
+
106
+ ## Next actions
107
+ - Inspect worker concurrency changes in deploy #784
108
+ ```
109
+
110
+ ## Principles
111
+
112
+ **Read-only** — Horus never writes to your production systems.
113
+
114
+ **Deterministic first** — The engine is deterministic; optional `--ai` adds an Anthropic narrative.
115
+
116
+ **Local-first** — Connectors read from your own clusters, not a hosted black box.
117
+
118
+ **Project-scoped** — Every investigation belongs to a specific project and environment.
119
+
120
+ ## Capabilities
121
+
122
+ Installable today. More connectors and AI providers are in progress.
123
+
124
+ **Today**
125
+
126
+ - Elasticsearch logs
127
+ - Grafana metrics
128
+ - MongoDB state
129
+ - BullMQ queue evidence
130
+ - Source intelligence (code graph)
131
+ - Timeline generation
132
+ - Evidence correlation
133
+ - Investigation replay
134
+ - Postmortem drafts
135
+
136
+ **Coming next**
137
+
138
+ - Kubernetes evidence
139
+ - Distributed trace reconstruction
140
+ - Slack evidence ingestion
141
+ - Local AI provider execution
142
+
143
+ ---
144
+
145
+ ## Architecture
146
+
147
+ Horus is organized in four layers:
148
+
149
+ **Source Intelligence**
150
+
151
+ - Horus source intelligence backend — code graph, semantic search, impact analysis, ownership.
152
+
153
+ **Runtime Evidence**
154
+
155
+ - **Elasticsearch** — logs → synthesized error-signature evidence
156
+ - **MongoDB** — application/operational state
157
+ - **Grafana** — metrics via its datasource proxy
158
+ - **Redis / BullMQ** — queue runtime state
159
+ - **Git** — change history, ownership signals
160
+
161
+ **Investigation** (deterministic)
162
+
163
+ - **Queue Stitcher** — connects producer `queue.add(...)` to consumer `@Processor` handlers
164
+ - **Timeline Engine** — orders evidence into a sequence of events
165
+ - **Correlation Engine** — connects evidence across sources into incident threads
166
+
167
+ **Presentation**
168
+
169
+ - **Deterministic investigation report** — evidence, timeline, hypotheses, gap analysis, next actions
170
+ - **Optional AI narrative** — a later layer on top of the deterministic report
171
+
172
+ ### Source intelligence is built into Horus
173
+
174
+ **Source intelligence is the expected intelligence layer used by Horus** — not an optional integration. Semantic search, impact analysis, ownership signals, change detection, and the process graph live in the Horus source intelligence backend; Horus does not duplicate them.
175
+
176
+ The **only** code-intelligence gap Horus owns is **queue-boundary stitching**: the source graph terminates around `queue.add(...)` and doesn't connect a producer to the consumer's `@Processor`. The stitcher synthesizes those producer → queue → worker edges.
177
+
178
+ > If the Horus source intelligence backend is unavailable, Horus can still collect runtime evidence, but source context, impact analysis, change analysis, and queue stitching become degraded.
179
+
180
+ Horus talks to the source intelligence backend over **HTTP/MCP only** (no CLI shell-outs for queries). Run `horus index` in a repository to start and register its source intelligence host.
181
+
182
+ ## Configuration
183
+
184
+ The config model separates **code** from **runtime**:
185
+
186
+ - **Code belongs to the project** — `repositories[]`, each served by its own source intelligence host.
187
+ - **Runtime belongs to the environment** — `environments[].connectors` (Elasticsearch, MongoDB, Grafana, Redis/BullMQ).
188
+
189
+ ```ts
190
+ // config/horus.config.ts
191
+ export default defineConfig({
192
+ projects: [
193
+ {
194
+ name: 'atlas-payments',
195
+ repositories: [
196
+ {
197
+ name: 'atlas-payments',
198
+ path: '/repos/atlas-payments',
199
+ source: { hostUrl: 'http://127.0.0.1:8420' },
200
+ },
201
+ ],
202
+ environments: [
203
+ {
204
+ name: 'production',
205
+ readOnly: true,
206
+ connectors: {
207
+ elasticsearch: {
208
+ indexPattern: 'atlas-payments-prod-*',
209
+ serviceName: 'atlas-payments-prod',
210
+ },
211
+ mongodb: {
212
+ database: 'atlas_payments_prod',
213
+ collections: ['orders', 'payments', 'workers'],
214
+ },
215
+ grafana: {},
216
+ },
217
+ },
218
+ ],
219
+ },
220
+ ],
221
+ database: {
222
+ url: process.env.DATABASE_URL ?? 'postgresql://horus:horus@localhost:5433/horus',
223
+ },
224
+ });
225
+ ```
226
+
227
+ **No connector runs without an explicit project/env scope** — there are no global connector defaults.
228
+
229
+ **Secrets are never committed.** Connector credentials are read from environment variables at runtime. Keep them in a gitignored file (e.g. `~/.horus.env`) and `source` it before running. For a full reference on which Horus files to commit and which to gitignore, see **[docs/gitignore-guide.md](./docs/gitignore-guide.md)**.
230
+
231
+ ## Install
232
+
233
+ See **[docs/install.md](./docs/install.md)** for full install, update, and uninstall instructions.
234
+
235
+ ```bash
236
+ curl -fsSL https://horus.sh/install.sh | bash
237
+ npm install -g @merittdev/horus
238
+ brew install meritt-dev/tap/horus
239
+ horus --version
240
+ horus setup
241
+ ```
242
+
243
+ The curl installer downloads the Horus CLI from GitHub Releases and attempts to install the Horus source intelligence backend. All three channels install the same `horus` binary.
244
+
245
+ ### What the installer installs
246
+
247
+ | Component | Role | Required |
248
+ | --- | --- | --- |
249
+ | **Horus CLI** | The `horus` command | Yes |
250
+ | **Horus source intelligence backend** | Enables `horus index`, `horus explain`, `horus changes`, `horus architecture` | Optional |
251
+
252
+ ### Prerequisites
253
+
254
+ | Requirement | Role |
255
+ | --- | --- |
256
+ | Node.js 22+ | Horus CLI runtime (the installed binary needs Node.js) |
257
+ | Postgres 16 | Investigation audit store — run locally via `docker compose up -d` or use a managed instance |
258
+ | Python 3.11+ + uv/pip | Required only for the Horus source intelligence backend |
259
+
260
+ The installer **does not** configure Elasticsearch, MongoDB, Grafana, Redis, or any production system. Runtime connectors are added per-project after install via `horus connect`.
261
+
262
+ ### Direct download (without the curl installer)
263
+
264
+ ```bash
265
+ # Replace vX.Y.Z with the current release tag
266
+ curl -fsSL https://github.com/meritt-dev/horus/releases/download/v0.1.0/horus-v0.1.0 -o horus
267
+ chmod +x horus
268
+ sudo mv horus /usr/local/bin/horus
269
+ horus --version
270
+ ```
271
+
272
+ To **update** to a newer version, re-run the installer — it overwrites the binary and leaves your config untouched. To **uninstall**, see **[docs/install.md#uninstall](./docs/install.md#uninstall)**.
273
+
274
+ If something goes wrong after install, see **[docs/troubleshooting.md](./docs/troubleshooting.md)**.
275
+
276
+ ## Local development
277
+
278
+ ```bash
279
+ pnpm install
280
+ docker compose up -d # Postgres 16 on localhost:5433
281
+ pnpm build # builds apps/horus/dist/index.cjs
282
+
283
+ # Per repository: start the source intelligence host and stitch queue boundaries
284
+ horus index
285
+
286
+ source ~/.horus.env
287
+
288
+ node apps/horus/dist/index.cjs status
289
+ ```
290
+
291
+ **Verify the full v0.1 user path (init → investigate → replay → postmortem):**
292
+
293
+ ```bash
294
+ # No-services startup check (version, help, doctor):
295
+ ./scripts/smoke-test.sh apps/horus/dist/index.cjs
296
+
297
+ # Full end-to-end flow (requires Postgres from docker compose up -d):
298
+ ./scripts/e2e-smoke.sh apps/horus/dist/index.cjs
299
+ ```
300
+
301
+ ```bash
302
+ horus --help
303
+ horus help <command>
304
+ horus investigate --help
305
+ ```
306
+
307
+ ### Core commands
308
+
309
+ | Command | What it does |
310
+ | --- | --- |
311
+ | `horus status [--project --env]` | Per-project/env connector-health matrix |
312
+ | `horus index --project <p> --env <e>` | Build the queue map (stitcher) for a project |
313
+ | `horus investigate --project <p> --env <e> "<hint>"` | Full deterministic investigation report |
314
+ | `horus logs [service] --project <p> --env <e>` | Error-signature evidence (`--raw` for lines) |
315
+ | `horus state --project <p> --env <e>` | MongoDB application-state evidence (read-only) |
316
+ | `horus metrics [hint] --project <p> --env <e>` | Grafana metrics evidence |
317
+ | `horus explain <symbol>` · `blast-radius` · `architecture` · `what-changed` | Source-aware code intelligence (requires source intelligence backend) |
318
+
319
+ ## Local project workflow (git-style)
320
+
321
+ A repo carries a `.horus/config.json` (discovered by walking up from the working directory, like `.git`), and a global registry (`~/.horus/registry.json`) lets `--name` resolve a project from anywhere.
322
+
323
+ ```bash
324
+ horus setup
325
+
326
+ cd /repos/atlas-payments
327
+ horus index
328
+
329
+ horus investigate "checkout latency spike"
330
+ horus investigate --name atlas-payments "checkout latency spike"
331
+ horus projects
332
+ ```
333
+
334
+ `horus index` reuses an already-running source intelligence host when one is healthy. Runtime connectors are added to the env block of `.horus/config.json` afterwards.
335
+
336
+ ## Layout
337
+
338
+ ```
339
+ packages/
340
+ core/ evidence model, config schema + project/env resolution, version pins
341
+ connectors/ provider contracts + source intelligence (HTTP/MCP) · Elasticsearch · Grafana · MongoDB · Git
342
+ stitcher/ queue-boundary stitcher
343
+ db/ Drizzle schema + migrations (plain Postgres, no pgvector)
344
+ engine/ deterministic investigation pipeline (timeline, correlation, hypotheses, gaps)
345
+ cli/ commander CLI
346
+ apps/horus/ composition root (bundled bin)
347
+ config/ horus.config.ts
348
+ ```
349
+
350
+ ## Foundation
351
+
352
+ - TypeScript monorepo (pnpm + Turborepo)
353
+ - Postgres + Drizzle — semantic search delegated to source intelligence backend
354
+ - Built-in **Horus source intelligence backend**, over HTTP/MCP only
355
+ - Project/environment-scoped connectors; read-only against production
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 = "0.1.0";
50317
+ var HORUS_VERSION = "0.1.2";
50318
50318
  var PINNED_AXON_VERSION = "1.0.1";
50319
50319
  var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
50320
50320
 
@@ -67040,6 +67040,22 @@ async function runIndex(opts) {
67040
67040
  ` investigate: horus investigate --name ${name} "<hint>" (or from this repo: horus investigate "<hint>")`
67041
67041
  )
67042
67042
  );
67043
+ } else if (spawned && !configuredHost) {
67044
+ const existingPath = discoverLocalConfig(root);
67045
+ if (existingPath) {
67046
+ const file = readLocalConfig(existingPath);
67047
+ const project = file.project;
67048
+ const repos = project["repositories"];
67049
+ if (repos && repos.length > 0) {
67050
+ repos[0]["source"] = { hostUrl };
67051
+ }
67052
+ writeLocalConfig(root, file);
67053
+ registerProject(label, root, existingPath);
67054
+ console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} \u2014 source host registered at ${hostUrl}`);
67055
+ console.log(import_picocolors3.default.dim(` ${existingPath}`));
67056
+ } else {
67057
+ console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`);
67058
+ }
67043
67059
  } else {
67044
67060
  console.log(
67045
67061
  `${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`
@@ -67435,20 +67451,23 @@ function factorRuntimeSignals(items, now) {
67435
67451
  const timestamps = items.filter((e) => e.timestamp !== void 0).map((e) => new Date(e.timestamp).getTime()).sort((a, b2) => b2 - a);
67436
67452
  const newestTs = timestamps[0];
67437
67453
  if (newestTs !== void 0) {
67438
- const ageMs = nowMs - newestTs;
67439
- if (ageMs <= 36e5) {
67440
- recencyDelta = 0.05;
67441
- recencyReason = "Evidence from within the last hour";
67442
- } else if (ageMs <= 864e5) {
67443
- recencyDelta = 0.02;
67444
- recencyReason = "Evidence from within the last 24 hours";
67445
- } else if (ageMs <= 2592e5) {
67446
- } else if (ageMs <= 6048e5) {
67447
- recencyDelta = -0.02;
67448
- recencyReason = "Most recent evidence is 3\u20137 days old \u2014 may predate this incident";
67449
- } else {
67450
- recencyDelta = -0.05;
67451
- recencyReason = "Most recent evidence is over 7 days old \u2014 likely predates this incident";
67454
+ const rawAgeMs = nowMs - newestTs;
67455
+ const ageMs = rawAgeMs < 0 ? rawAgeMs >= -3e5 ? 0 : null : rawAgeMs;
67456
+ if (ageMs !== null) {
67457
+ if (ageMs <= 36e5) {
67458
+ recencyDelta = 0.05;
67459
+ recencyReason = "Evidence from within the last hour";
67460
+ } else if (ageMs <= 864e5) {
67461
+ recencyDelta = 0.02;
67462
+ recencyReason = "Evidence from within the last 24 hours";
67463
+ } else if (ageMs <= 2592e5) {
67464
+ } else if (ageMs <= 6048e5) {
67465
+ recencyDelta = -0.02;
67466
+ recencyReason = "Most recent evidence is 3\u20137 days old \u2014 may predate this incident";
67467
+ } else {
67468
+ recencyDelta = -0.05;
67469
+ recencyReason = "Most recent evidence is over 7 days old \u2014 likely predates this incident";
67470
+ }
67452
67471
  }
67453
67472
  }
67454
67473
  let recurrenceDelta = 0;
@@ -71906,6 +71925,36 @@ async function runReplay(id, opts) {
71906
71925
  const fmt = opts.format ?? "text";
71907
71926
  const out = fmt === "json" ? reportToJSON(report) : fmt === "markdown" || fmt === "md" ? reportToMarkdown(report) : renderReport2(report);
71908
71927
  console.log(out);
71928
+ if (opts.ai && fmt !== "json") {
71929
+ const narrativeInput = buildNarrativeInput(report);
71930
+ const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
71931
+ const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
71932
+ if (!fromProvider) {
71933
+ console.error(import_picocolors13.default.yellow("[ai] Provider unavailable \u2014 deterministic output shown above."));
71934
+ if (validationErrors?.length) {
71935
+ console.error(import_picocolors13.default.dim(` ${validationErrors[0]}`));
71936
+ }
71937
+ } else {
71938
+ const sep = "\u2500".repeat(60);
71939
+ console.log(`
71940
+ ${sep}`);
71941
+ console.log(import_picocolors13.default.bold("AI Narrative"));
71942
+ console.log(sep);
71943
+ console.log(import_picocolors13.default.bold("What:"), output.what);
71944
+ console.log(import_picocolors13.default.bold("Why:"), output.why);
71945
+ if (output.whereNext.length > 0) {
71946
+ console.log(import_picocolors13.default.bold("Next steps:"));
71947
+ for (const step of output.whereNext) {
71948
+ console.log(` \u2022 ${step}`);
71949
+ }
71950
+ }
71951
+ if (output.citations.length > 0) {
71952
+ console.log(import_picocolors13.default.dim(`
71953
+ Cited evidence: ${output.citations.map((c) => c.evidenceId).join(", ")}`));
71954
+ }
71955
+ console.log(import_picocolors13.default.dim(`AI confidence: ${(output.confidence * 100).toFixed(0)}%`));
71956
+ }
71957
+ }
71909
71958
  } finally {
71910
71959
  await sql2.end();
71911
71960
  }
@@ -71963,7 +72012,39 @@ async function runPostmortem(id, opts) {
71963
72012
  }
71964
72013
  report = migrateReport(row.report);
71965
72014
  }
71966
- const content = generatePostmortem(report);
72015
+ let content = generatePostmortem(report);
72016
+ if (opts.aiSummary) {
72017
+ const narrativeInput = buildNarrativeInput(report);
72018
+ const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
72019
+ const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
72020
+ if (!fromProvider) {
72021
+ content += `
72022
+
72023
+ ## AI Summary
72024
+
72025
+ _AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
72026
+ `;
72027
+ } else {
72028
+ content += "\n\n## AI Summary\n\n";
72029
+ content += `**What happened:** ${output.what}
72030
+
72031
+ `;
72032
+ content += `**Why:** ${output.why}
72033
+ `;
72034
+ if (output.whereNext.length > 0) {
72035
+ content += "\n**Next steps:**\n";
72036
+ for (const step of output.whereNext) {
72037
+ content += `- ${step}
72038
+ `;
72039
+ }
72040
+ }
72041
+ if (output.citations.length > 0) {
72042
+ content += `
72043
+ **Cited evidence:** ${output.citations.map((c) => c.evidenceId).join(", ")}
72044
+ `;
72045
+ }
72046
+ }
72047
+ }
71967
72048
  if (opts.output) {
71968
72049
  const outputPath = (0, import_node_path7.resolve)(opts.output);
71969
72050
  if ((0, import_node_fs6.existsSync)(outputPath) && !opts.force) {
@@ -72517,7 +72598,7 @@ async function runInit(opts) {
72517
72598
  const name = opts.name ?? (0, import_node_path8.basename)(root);
72518
72599
  const envName = opts.env ?? "production";
72519
72600
  const repo = { name, path: root };
72520
- if (opts.axon) repo["axon"] = { hostUrl: opts.axon };
72601
+ if (opts.source) repo["source"] = { hostUrl: opts.source };
72521
72602
  const file = {
72522
72603
  version: 1,
72523
72604
  project: {
@@ -72531,9 +72612,9 @@ async function runInit(opts) {
72531
72612
  console.log(`${import_picocolors23.default.green("\u2713")} Initialized Horus project ${import_picocolors23.default.bold(name)}`);
72532
72613
  console.log(import_picocolors23.default.dim(` config: ${configPath}`));
72533
72614
  console.log(import_picocolors23.default.dim(` registered: horus investigate --name ${name} "<hint>"`));
72534
- if (!opts.axon) {
72615
+ if (!opts.source) {
72535
72616
  console.log(
72536
- import_picocolors23.default.dim(" no source-intelligence host set \u2014 run `horus index` to analyze + host, or pass --axon <url>")
72617
+ import_picocolors23.default.dim(" no source-intelligence host set \u2014 run `horus index` to analyze + host, or pass --source <url>")
72537
72618
  );
72538
72619
  }
72539
72620
  console.log(
@@ -73301,7 +73382,7 @@ async function runDoctor(opts) {
73301
73382
  const project = file.project;
73302
73383
  const repos = project["repositories"];
73303
73384
  const hasHost = repos?.some(
73304
- (r) => r["axon"]?.["hostUrl"]
73385
+ (r) => r["source"]?.["hostUrl"] ?? r["axon"]?.["hostUrl"]
73305
73386
  );
73306
73387
  if (hasHost) {
73307
73388
  checks.push({ label: "Source-intelligence host", status: "pass", detail: "configured" });
@@ -73310,7 +73391,7 @@ async function runDoctor(opts) {
73310
73391
  label: "Source-intelligence host",
73311
73392
  status: "warn",
73312
73393
  detail: "not configured",
73313
- next: "run `horus index` to analyze this repo and start a host, or pass --axon <url> to `horus init`"
73394
+ next: "run `horus index` to analyze this repo and start a host, or pass --source <url> to `horus init`"
73314
73395
  });
73315
73396
  }
73316
73397
  } catch {
@@ -73566,7 +73647,7 @@ function configTemplate(name, repoPath) {
73566
73647
  repositories: [{
73567
73648
  name: '${name}',
73568
73649
  path: '${repoPath}',
73569
- axon: { hostUrl: 'http://127.0.0.1:8420' },
73650
+ source: { hostUrl: 'http://127.0.0.1:8420' },
73570
73651
  }],
73571
73652
  environments: [{
73572
73653
  name: 'production',
@@ -73828,9 +73909,14 @@ Examples:
73828
73909
  program2.command("setup").description("Verify prerequisites (source-intelligence backend + Postgres) and guide any fixes").option("-c, --config <path>", "path to horus.config.ts").action(async (opts) => {
73829
73910
  process.exitCode = await runSetup(opts);
73830
73911
  });
73831
- program2.command("init").description("Create a local .horus/config.json for this repo and register it").option("--name <name>", "project name (default: repo directory name)").option("--env <name>", "environment name (default: production)").option("--axon <url>", "Source-intelligence host URL for this repo (e.g. http://127.0.0.1:8420)").option("--path <dir>", "repository root (default: nearest git root, else cwd)").action(
73912
+ program2.command("init").description("Create a local .horus/config.json for this repo and register it").option("--name <name>", "project name (default: repo directory name)").option("--env <name>", "environment name (default: production)").option("--source <url>", "source-intelligence host URL for this repo (e.g. http://127.0.0.1:8420)").addOption(new Option("--axon <url>", "deprecated alias for --source").hideHelp()).option("--path <dir>", "repository root (default: nearest git root, else cwd)").action(
73832
73913
  async (opts) => {
73833
- process.exitCode = await runInit(opts);
73914
+ process.exitCode = await runInit({
73915
+ name: opts.name,
73916
+ env: opts.env,
73917
+ source: opts.source ?? opts.axon,
73918
+ path: opts.path
73919
+ });
73834
73920
  }
73835
73921
  ).addHelpText("after", `
73836
73922
  Examples:
@@ -73995,23 +74081,27 @@ Examples:
73995
74081
  horus investigations
73996
74082
  horus investigations -n 20
73997
74083
  `);
73998
- program2.command("replay <id>").description("Re-render a saved investigation from the audit store (no re-query)").option("-c, --config <path>", "path to horus.config.ts").option("--format <fmt>", "text | markdown | json", "text").action(async (id, opts) => {
73999
- process.exitCode = await runReplay(id, { config: opts.config, format: opts.format });
74084
+ program2.command("replay <id>").description("Re-render a saved investigation from the audit store (no re-query)").option("-c, --config <path>", "path to horus.config.ts").option("--format <fmt>", "text | markdown | json", "text").option("--ai", "enrich report with AI narrative (requires ANTHROPIC_API_KEY; falls back to deterministic on failure)").option("--ai-model <model>", "AI model for --ai (default: claude-opus-4-8)").action(async (id, opts) => {
74085
+ process.exitCode = await runReplay(id, { config: opts.config, format: opts.format, ai: opts.ai, aiModel: opts.aiModel });
74000
74086
  }).addHelpText("after", `
74001
74087
  Examples:
74002
74088
  horus replay <id>
74003
74089
  horus replay <id> --format markdown
74004
74090
  horus replay <id> --format json
74091
+ horus replay <id> --ai
74092
+ horus replay <id> --ai --ai-model claude-sonnet-4-6
74005
74093
 
74006
74094
  (Use 'horus investigations' to list saved investigation ids.)
74007
74095
  `);
74008
- program2.command("postmortem <id>").description("Draft an editable incident postmortem from a saved investigation").option("-c, --config <path>", "path to horus.config.ts").option("--output <path>", "write Markdown to a file instead of printing to stdout").option("--force", "overwrite the output file if it already exists").action(async (id, opts) => {
74009
- process.exitCode = await runPostmortem(id, { config: opts.config, output: opts.output, force: opts.force });
74096
+ program2.command("postmortem <id>").description("Draft an editable incident postmortem from a saved investigation").option("-c, --config <path>", "path to horus.config.ts").option("--output <path>", "write Markdown to a file instead of printing to stdout").option("--force", "overwrite the output file if it already exists").option("--ai-summary", "append an AI-generated summary section (requires ANTHROPIC_API_KEY; falls back gracefully)").option("--ai-model <model>", "AI model for --ai-summary (default: claude-opus-4-8)").action(async (id, opts) => {
74097
+ process.exitCode = await runPostmortem(id, { config: opts.config, output: opts.output, force: opts.force, aiSummary: opts.aiSummary, aiModel: opts.aiModel });
74010
74098
  }).addHelpText("after", `
74011
74099
  Examples:
74012
74100
  horus postmortem <id>
74013
74101
  horus postmortem <id> --output ./postmortem.md
74014
74102
  horus postmortem <id> --output ./postmortem.md --force
74103
+ horus postmortem <id> --ai-summary
74104
+ horus postmortem <id> --output ./postmortem.md --ai-summary --ai-model claude-sonnet-4-6
74015
74105
 
74016
74106
  (Use 'horus investigations' to list saved investigation ids.)
74017
74107
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "build": "tsup",
14
14
  "typecheck": "tsc --noEmit",
15
15
  "test": "tsup && vitest run --passWithNoTests",
16
- "prepublishOnly": "tsup"
16
+ "prepublishOnly": "tsup",
17
+ "prepack": "cp ../../README.md README.md"
17
18
  },
18
19
  "engines": {
19
20
  "node": ">=22"