Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions openspec/changes/add-user-documentation/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
schema: spec-driven
created: 2026-09-29
skip_specs: true
98 changes: 98 additions & 0 deletions openspec/changes/add-user-documentation/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Design

## Context

The only user documentation is `README.md` (about 625 lines, with 336 lines of
uncommitted additions from `add-notifications`). Reference facts live in three other
places: the Go source (`config.go`, `config_sinks.go`, `notify_metrics.go`, the flag
sets in `main.go`), the archived specs under `openspec/specs/`, and
`tailnet-reachability-monitor-design.md`. `examples/tail-watch.yaml` and
`examples/compose.yaml` are the only runnable samples. markdownlint runs with MD013 at
90 columns (tables and code blocks exempt) and ignores `.claude/**` and `node_modules/**`.

## Goals / Non-Goals

**Goals:**

- Every operator question maps to exactly one page: learn, do, look up, or understand.
- Every command and YAML snippet in the docs is checked against the built binary, not
copied from memory.
- A fact lives in one page; other pages link to it.

**Non-Goals:**

- Documenting internal architecture (`netEnv`, clocks, mutex layout). That stays in
`CLAUDE.md` and `openspec/`.
- Auto-generating reference pages from code. The config surface is small enough to write
by hand and check.

## Decisions

### Diataxis layout under `docs/`

```text
docs/
README.md index, one line per page
tutorial/first-monitor.md
how-to/ deploy.md acls.md remote-subnets.md
slack-sink.md webhook-sink.md troubleshoot.md
reference/ config.md checks.md cli.md metrics.md payload.md
explanation/ failure-layers.md alert-state.md incidents-and-queues.md
contributing/ live-notification-test.md releases.md
```

Alternative: a single long `docs/guide.md`. Rejected because it recreates the README
problem and gives readers no stable links.

### README becomes a landing page

Keep the purpose paragraph, requirements summary, a five-line quickstart, and a link
list. Alternative: keep the README complete and add `docs/` as extras. Rejected: two
copies of the sink options and payload drift as soon as one changes.

### Accuracy is checked mechanically where possible

- Each YAML snippet that is a full config is saved and run through
`tail-watch check-config`.
- Each shell command is run, or, when it needs a tailnet, is marked as such and matches a
command exercised by `scripts/live-stack.sh` or the live tests.
- Metric names in `reference/metrics.md` are compared against the `Name:` entries in
`notify_metrics.go` and the collector metrics, so none is documented that does not exist.

Alternative: extract snippets into Go tests. Rejected as over-built for a docs change.

### Source of truth per page

| Page | Source |
| ---- | ------ |
| `reference/config.md` | `config.go`, `config_sinks.go`, design doc "Config" |
| `reference/cli.md` | flag sets in `main.go`, `notifytest.go` |
| `reference/metrics.md` | metric registrations, design doc "Metrics" |
| `reference/payload.md` | `event.go`, `testdata/payload` golden files |
| `explanation/*` | archived specs, design doc "What a failed check means", "Alert state machine" |

Where code and design doc disagree, the code and archived spec win and the page follows
them; note the mismatch in the task that found it.

### Tutorial uses the shipped example

The tutorial starts from `examples/compose.yaml`, adds one failing target, and ends with
the alert arriving (Slack webhook or a local webhook receiver). This keeps it runnable
without inventing a new sample. Alternative: a self-contained tutorial with its own
compose file. Deferred: revisit only if the example proves too far from a first run.

## Risks / Trade-offs

- [Docs describe behavior that `add-notifications` still changes] → Apply after that
change is archived; the README diff is the input.
- [Restructure loses README content] → Move first, then trim; a task diffs old README
headings against the new pages and confirms each is accounted for.
- [Existing README anchors are linked from elsewhere] → Search the repo and release notes
for `README.md#` links and keep or redirect them.
- [Reference pages drift from code over time] → The metrics/config checks above are
repeatable, and `CLAUDE.md` gets a note to update `docs/` with config or metric
changes.

## Open Questions

- Should `docs/` link check run in CI, or only locally in the validation group?
72 changes: 72 additions & 0 deletions openspec/changes/add-user-documentation/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Proposal

## Why

`README.md` has grown to about 625 lines and carries four kinds of content at once:
explanation (failure layers, alert state), how-to (Docker, ACLs, Slack webhook), reference
(checks, sink options, payload) and contributor notes (live notification test,
releases). A new operator has no path from "what is this" to "first alert in Slack", and
nothing documents the full `tail-watch.yaml` schema, the `tailwatch_*` metrics, or the
flags in one place. Phase 1 is feature-complete once `add-notifications` lands, so the
behavior is stable enough to document now.

## What Changes

- Restructure user documentation into `docs/` following the Diataxis quadrants
(tutorial, how-to, reference, explanation), written with the `documentation-writer`
skill.
- Reduce `README.md` to a short overview: what tail-watch is, a five-line quickstart,
and links into `docs/`. Move the existing README material into the matching pages
rather than keeping two copies.
- Add pages that do not exist today: a first-monitor tutorial, a configuration reference,
a metrics reference, a CLI reference (flags, `check-config`, `notify-test`), and a
troubleshooting guide.
- Split contributor material (live notification test, development and releases) into
`docs/contributing/` so it is out of the operator path.
- Lint the new Markdown with markdownlint and keep it in the existing lint task.

This change adds documentation only. It does not change code, config keys, metric names or
payload fields; every documented fact is checked against the source, the archived specs
and `tailnet-reachability-monitor-design.md`.

Implements no design-doc section. The design doc has no user-documentation section, and
documentation is not a phase 1 deliverable there. It describes behavior from the design
doc sections "Check types", "What a failed check means", "Alert state machine",
"Notifications", "Config", "ACLs" and "Metrics".

Phase: MVP (phase 1). Non-goals:

- Phase 2 and 3 features (PagerDuty, Slack bot-token mode, `target.degraded`, quorum
across collectors, tsnet mode). Pages mention them only as "not yet supported".
- A generated documentation site or hosting (mkdocs, Hugo, GitHub Pages).
- Man pages, shell completion, or in-binary help text changes.
- Translating or versioning the docs per release.

## Capabilities

### New Capabilities

None. Documentation does not change system behavior, so no spec changes. The change sets
`skip_specs: true` in `.openspec.yaml`.

### Modified Capabilities

None.

## Impact

- New: `docs/` tree (tutorial, how-to, reference, explanation, contributing).
- Changed: `README.md` (slimmed), `CLAUDE.md` (layout note for `docs/`), markdownlint
configuration if `docs/` needs exclusions or the line-length rule needs adjusting.
- No Go code, dependency, or runtime impact.
- Sequencing: this change should be applied after `add-notifications` is archived, so the
notification pages describe final behavior. The uncommitted README edits from that
change are the source for those pages.

## Assumptions

- The primary reader is a tailnet admin or SRE who knows Tailscale ACLs but not tail-watch.
- The README is restructured, not left alongside new pages.
- The tutorial builds on `examples/compose.yaml` and `examples/tail-watch.yaml`.

These were proposed during exploration and not contradicted; revise them here if wrong.
53 changes: 53 additions & 0 deletions openspec/changes/add-user-documentation/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Tasks

## 1. Outline and scaffolding

- [ ] 1.1 Use the `documentation-writer` skill to confirm audience, goal and scope per page, and get the outline approved; verify the approved outline matches the tree in `design.md`
- [ ] 1.2 Create `docs/README.md` index and the empty quadrant directories; verify every planned page is listed and links resolve
- [ ] 1.3 Add a heading-by-heading map from current `README.md` sections to target pages (kept in the change directory, not committed to `docs/`); verify no README section is unassigned
- [ ] 1.4 Search the repo for `README.md#` anchors and inbound links (`CLAUDE.md`, `CHANGELOG.md`, `examples/`); verify each is listed with its new target

## 2. Reference pages

- [ ] 2.1 Write `docs/reference/config.md` from `cmd/tail-watch/config.go` and `config_sinks.go`; verify every key in `examples/tail-watch.yaml` and every `sinks` key appears with type, default and required/optional
- [ ] 2.2 Save each full-config snippet from `docs/reference/config.md` and run `tail-watch check-config`; verify all pass
- [ ] 2.3 Write `docs/reference/cli.md` (flags, `TAIL_WATCH_CONFIG`, `TAIL_WATCH_LOG_LEVEL`, `check-config`, `notify-test`, exit codes) from `cmd/tail-watch/main.go` and `notifytest.go`; verify against `tail-watch --help` and `notify-test --help`
- [ ] 2.4 Write `docs/reference/metrics.md` from the metric registrations under `cmd/tail-watch/`; verify each documented `tailwatch_*` name appears in the source and each registered name is documented
- [ ] 2.5 Write `docs/reference/checks.md` (ICMP, TCP, HTTP, tsping options and semantics) from `checks.go`; verify against the `reachability-checks` spec
- [ ] 2.6 Write `docs/reference/payload.md` (event types, fields, severities, signature header) from `event.go` and `testdata/payload`; verify the field list matches the golden files

## 3. Explanation pages

- [ ] 3.1 Write `docs/explanation/failure-layers.md` from the README "Failure layers" section and the `failure-classification` and `dependency-learning` specs; verify each layer name matches the `failure_layer` metric label values
- [ ] 3.2 Write `docs/explanation/alert-state.md` covering grace, flap suppression and dependency suppression, from the `alert-state-machine` and `dependency-suppression` specs; verify each state and timer name matches the config keys in `reference/config.md`
- [ ] 3.3 Write `docs/explanation/incidents-and-queues.md` covering `state_dir`, at-least-once delivery, replay and open incidents; verify against the `add-notifications` spec set once archived

## 4. How-to guides

- [ ] 4.1 Write `docs/how-to/deploy.md` (binary, Docker, Compose without a dedicated host) from README "Build and run" and "Docker"; verify the Docker build command runs and `--version` prints
- [ ] 4.2 Write `docs/how-to/acls.md` from README "ACLs"; verify the policy snippet is valid HuJSON
- [ ] 4.3 Write `docs/how-to/remote-subnets.md` from README "Remote subnets and local LANs"; verify each `network` value and startup failure named in it exists in `netenv.go` and `config.go`
- [ ] 4.4 Write `docs/how-to/slack-sink.md` including getting a webhook URL and using `*_env`; verify the config snippet passes `check-config` and `notify-test --sink` steps match `notifytest.go`
- [ ] 4.5 Write `docs/how-to/webhook-sink.md` (signing, headers, batching, retries); verify the signature verification example against `webhook_test.go`
- [ ] 4.6 Write `docs/how-to/troubleshoot.md` (userspace tailscaled refusal, missing `tag:monitor`, ICMP permissions, socket path, missing secrets, corrupt state); verify each symptom's message text against the source strings

## 5. Tutorial

- [ ] 5.1 Write `docs/tutorial/first-monitor.md` starting from `examples/compose.yaml`, adding a failing target, and ending with a delivered alert; verify by following it on a clean checkout with `scripts/live-stack.sh` or the Compose file
- [ ] 5.2 Record any step that needed a fix while following it, and update the page; verify a second read-through needs no unwritten step

## 6. README and contributor split

- [ ] 6.1 Move "Live notification test" and "Development and releases" from `README.md` to `docs/contributing/`; verify commands still match `CLAUDE.md` and `scripts/live-stack.sh`
- [ ] 6.2 Trim `README.md` to overview, quickstart and links; verify against the 1.3 map that every removed section exists in a page
- [ ] 6.3 Fix or redirect the inbound links found in 1.4; verify a grep for the old anchors finds none
- [ ] 6.4 Add a `docs/` layout and "update docs with config or metric changes" note to `CLAUDE.md`; verify the note matches the final tree
- [ ] 6.5 Add a `CHANGELOG.md` entry if the release process lists documentation; verify against `release-please-config.json`

## 7. Validation

- [ ] 7.1 Run `mise exec -- go build ./...`, `go vet ./...` and `go test ./...`; verify all pass, confirming no code changed
- [ ] 7.2 Run `mise run lint` (golangci-lint) and `govulncheck ./...`; verify no findings
- [ ] 7.3 Run `markdownlint` over `README.md`, `docs/` and `openspec/changes/add-user-documentation/`; verify zero findings
- [ ] 7.4 Run a link check over `docs/` and `README.md` (local, relative links); verify none are broken
- [ ] 7.5 Run `mise exec -- openspec validate add-user-documentation --strict`; verify it passes
Loading