Skip to content

Commit f7e33d9

Browse files
committed
docs(shadow): trends + alerting guide, CLI reference, ADR-0018, roadmap v1.5
1 parent f325a5c commit f7e33d9

4 files changed

Lines changed: 101 additions & 3 deletions

File tree

docs/ROADMAP.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,16 @@ regression test, replayed nightly; drift pages you before your users notice.
132132
empty corpus; `--report` / `--update-baseline` / `--threshold`
133133
- [x] Nightly GitHub Action template (`examples/workflows/volo-nightly.yml`)
134134
- [x] Acceptance: a seeded nondeterminism regression trips the alert (test-proven)
135-
- [ ] M14: drift trends + dashboard screens + webhook/Slack alerting
135+
136+
## v1.5.0 — M14: drift trends + dashboard + alerting ✅
137+
Goal: the sentinel's memory — reliability over time, visible and loud.
138+
139+
- [x] `SnapshotHistory` (append-only JSONL; every `shadow check` appends snapshot + drift verdict)
140+
with fleet-average and per-trace trend series (ADR-0018)
141+
- [x] `volo shadow trend` — ASCII sparkline per dimension; `--trace` follows one banked trace
142+
- [x] Webhook alerting on `shadow check` (`--webhook` / `VOLO_SHADOW_WEBHOOK`) — Slack-compatible
143+
payload, best-effort delivery (never masks the exit-3 alert)
144+
- [x] API: `GET /shadow/history` (+ per-trace) — the trend feed
145+
- [x] Dashboard: `/shadow` screen — fleet-average sparklines per dimension, drifted-night chips,
146+
banked-corpus table (bible §8.3 aesthetic, reuses the CI sparkline)
147+
- [ ] M15: red-team corpus v1 (attack scenarios + safety annex on the report)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# ADR 0018: trend history is append-only JSONL; alerts stay exit-code-first
2+
3+
- Status: accepted
4+
- Date: 2026-07-04
5+
6+
## Context
7+
8+
M14 gives the drift sentinel (ADR-0017) a memory and a voice: reliability-over-time for the
9+
dashboard/CLI, and a push alert. The history format is a data contract (the API, dashboard, and
10+
users' tooling read it), so it deserves a recorded decision.
11+
12+
## Decision
13+
14+
1. **History is append-only JSONL** (`./.volo/shadow-history.jsonl`): one
15+
`{at, snapshot, drift}` line per `volo shadow check`, including the baseline-establishing
16+
run (`drift: null`). Torn lines are skipped on read, so a crashed run can't poison the
17+
history. Committable and greppable, consistent with ADR-0017's no-database stance.
18+
2. **Two derived series, computed on read:** `fleet_series` (each dimension averaged across all
19+
banked traces, per check — the dashboard headline) and `trace_series(run_id)` (one banked
20+
trace over time). Nothing is precomputed or stored twice.
21+
3. **Alerting stays exit-code-first.** The webhook (`--webhook` / `VOLO_SHADOW_WEBHOOK`) is a
22+
loud *secondary* path: a Slack-compatible payload (`text` headline + full report under
23+
`volo`), delivered best-effort — a dead webhook logs a warning and never masks the exit-3
24+
alert. Stdlib `urllib` only; no HTTP dependency.
25+
4. **Dashboard/API surface:** `GET /shadow/history` returns `{checks: fleet_series, corpus:
26+
bank inventory}`; `GET /shadow/history/{run_id}` returns one trace's series. The `/shadow`
27+
web screen renders fleet-average sparklines per dimension plus drifted-night chips, reusing
28+
the CI sparkline component.
29+
30+
## Consequences
31+
32+
- The history file grows without bound (~1–2 KB per check per 10 traces); rotation/compaction
33+
is deliberately deferred until a real corpus shows the growth rate. Append-only means
34+
rotation is a safe external `mv`.
35+
- Averaging the fleet hides a single trace regressing among many healthy ones in the *chart*;
36+
the alert does not average — `compare` runs per trace, so the exit code still fires. The
37+
per-trace series exists for the drill-down.
38+
- Slack compatibility via `text` keeps zero config for the most common webhook; consumers
39+
needing richer formats read the `volo` key.
40+
41+
## Alternatives considered
42+
43+
- **SQLite for history** — rejected for now: the query needs (append, scan) fit JSONL; SQLite
44+
adds locking/migration surface for no current query we can't do in one pass.
45+
- **Alert-only-webhook (no exit code change)** — rejected: CI schedulers act on exit codes;
46+
webhooks fail silently.
47+
- **Storing derived series** — rejected: two sources of truth; recompute is O(history) and fast.

website/cli.mdx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,18 @@ uv run volo shadow list
8181
## `volo shadow check`
8282
Replay the whole corpus against the current agent build; compare the reliability surface to the
8383
last snapshot. Exit 0 = no drift, **exit 3 = drift alert** (dimension dropped > threshold or a
84-
verdict flipped). First run establishes the baseline.
84+
verdict flipped). First run establishes the baseline. Every check appends to the trend history;
85+
`--webhook` (or `VOLO_SHADOW_WEBHOOK`) POSTs a Slack-compatible alert on drift.
8586
```bash
86-
uv run volo shadow check --agent <module:fn> [--threshold 0.05] [--report drift.json] [--update-baseline]
87+
uv run volo shadow check --agent <module:fn> [--threshold 0.05] [--report drift.json] \
88+
[--update-baseline] [--history ./.volo/shadow-history.jsonl] [--webhook URL]
89+
```
90+
91+
## `volo shadow trend`
92+
Reliability-over-time from the check history: one ASCII sparkline per dimension, fleet average
93+
by default, `--trace <run_id>` for a single banked trace. Feeds the `/shadow` dashboard screen.
94+
```bash
95+
uv run volo shadow trend [--history PATH] [--trace RUN_ID]
8796
```
8897

8998
## `volo scenarios`

website/shadow.mdx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,36 @@ shadow check: 2 drift finding(s) -> ALERT
5959
`--threshold` tunes sensitivity (default 0.05), `--report drift.json` writes the machine-readable
6060
verdict, `--update-baseline` accepts the new surface as normal after an intentional change.
6161

62+
## Trends: reliability over time
63+
64+
Every check appends its snapshot to a JSONL history (default `./.volo/shadow-history.jsonl`), so
65+
the surface becomes a time series for free:
66+
67+
```bash
68+
uv run volo shadow trend
69+
# shadow trend: fleet average (12 trace(s)) over 30 check(s)
70+
# shadow trend: decision_determinism [@@@@@@@@@@@@@@@@@@@@@@@@=====] latest 0.500
71+
# shadow trend: faithfulness [@@@@@@@@@@@@@@@@@@@@@@@@@@@@@] latest 0.940
72+
# shadow trend: 5 check(s) drifted
73+
```
74+
75+
`--trace <run_id>` follows one banked trace instead of the fleet average. The same history feeds
76+
the dashboard: the **Shadow** screen (`/shadow`) plots each dimension's fleet average per check,
77+
flags drifted nights, and lists the banked corpus.
78+
79+
## Alerting
80+
81+
Exit code 3 is the primary alert — any scheduler can page on it. For a loud path, add a webhook
82+
(Slack-compatible: incoming webhooks read the `text` field; the full drift report rides along
83+
under `volo`):
84+
85+
```bash
86+
uv run volo shadow check --agent my_package.agent:run \
87+
--webhook "$SLACK_WEBHOOK_URL" # or set VOLO_SHADOW_WEBHOOK
88+
```
89+
90+
Webhook delivery is best-effort by design: a dead webhook never masks the exit-3 alert itself.
91+
6292
## Run it every night
6393

6494
A ready-made workflow lives at

0 commit comments

Comments
 (0)