@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.
- package/README.md +355 -0
- package/dist/index.cjs +118 -28
- 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.
|
|
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
|
|
67439
|
-
|
|
67440
|
-
|
|
67441
|
-
|
|
67442
|
-
|
|
67443
|
-
|
|
67444
|
-
|
|
67445
|
-
|
|
67446
|
-
|
|
67447
|
-
|
|
67448
|
-
|
|
67449
|
-
|
|
67450
|
-
|
|
67451
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 --
|
|
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 --
|
|
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
|
-
|
|
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("--
|
|
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(
|
|
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.
|
|
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"
|