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
69 changes: 48 additions & 21 deletions docs/guides/authoring-hubs.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,28 +127,39 @@ surf verify --follow # renamed symbol OR moved file: re-point the anc
Verifying without reading is the failure mode the whole tool exists to prevent. A green gate
promises only "nothing anchored changed since last sign-off" — never that the prose is true.

## Hubs and `AGENTS.md`
## Where claims can live

Hubs are *declarative* domain briefings; `AGENTS.md` is *imperative* operating instructions for
coding agents. Keep them separate — don't copy hub prose into `AGENTS.md`. Instead, give
`AGENTS.md` a pointer block that sends agents to the hubs directory to search for what they need:
A hub isn't a special file *type* — it's **any file the `hubs` glob matches that parses as a hub**
(a `---`-fenced `anchors:` frontmatter block + a markdown body). Claims don't have to live under
`hubs/`: add any file to the glob in `surf.toml`, give it the frontmatter, and `surf` treats it
like any other hub. The same is true for `AGENTS.md` or `CLAUDE.md`.

The common question is `AGENTS.md` — the *imperative* operating instructions for coding agents,
versus hubs, which are *declarative* domain briefings. There are two approaches, and **a central
`hubs/` directory is the recommended default.**

### Recommended — keep hubs and `AGENTS.md` separate

Keep the two concerns apart: don't copy hub prose into `AGENTS.md`. Instead, give `AGENTS.md` a
pointer block that sends agents to the hubs directory to search for what they need:

```markdown
<!-- surf:hubs -->
Context lives in [`hubs/`](./hubs/) — read only the hub(s) you need.
<!-- /surf:hubs -->
```

When that block is present, `surf lint` checks it links the configured hubs directory and that
the directory exists. It deliberately does **not** enumerate individual hubs — that would push an
agent to read everything instead of the one hub it needs.
When that block is present, `surf lint` checks it links the configured hubs directory and that the
directory exists. It deliberately does **not** enumerate individual hubs — that would push an agent
to read everything instead of the one hub it needs.

This keeps `AGENTS.md` lean, stops the declarative and imperative content from drifting into each
other, and keeps verification metadata out of the file agents read as instructions.

## Claims in `AGENTS.md` / `CLAUDE.md`
### Alternative — fold claims into `AGENTS.md` / `CLAUDE.md`

A hub isn't a special file *type* — it's any file the `hubs` glob matches that parses as a hub
(frontmatter `anchors:` block + markdown body). So you *can* add `AGENTS.md` or `CLAUDE.md` to the
glob and give it hub frontmatter, making one file double as both agent instructions and verified
claims:
If you'd rather keep the instructions and the verified claims about them in one file, add it to the
glob and give it hub frontmatter:

```toml
# surf.toml
Expand All @@ -169,16 +180,32 @@ anchors:
```

`surf verify` then hash-checks that claim like any other hub, and `surf for <path>` reports
`AGENTS.md` as anchoring into it. Two things to know before you do this:

- **The whole file must parse as a hub.** The `---` frontmatter has to be the top block and
unknown fields are rejected — you can't sprinkle a claim mid-document. Confirm whatever consumes
`AGENTS.md`/`CLAUDE.md` tolerates YAML frontmatter (most agent runners ignore it).
- **It couples the file to code structure.** Renaming an anchored symbol will trip the gate on
`AGENTS.md` as anchoring into it. Three things to weigh:

- **The whole file must parse as a hub.** The `---` frontmatter has to be the top block and unknown
fields are rejected — you can't sprinkle a claim mid-document.
- **The frontmatter is part of what the agent reads.** Most agent runners load `AGENTS.md` /
`CLAUDE.md` as raw text and don't strip YAML frontmatter, so the `anchors:` block lands in the
agent's context as a little extra noise. It's small and structured, but if you want `AGENTS.md`
to stay purely instructions, prefer the recommended approach above.
- **It couples the file to code structure.** Renaming an anchored symbol trips the gate on
`AGENTS.md` — that's the point of Surface, but it means an agent-docs file now participates in CI.

This is a trade-off, not a recommendation. The default split above — declarative hubs, imperative
`AGENTS.md` with a pointer block — keeps the two concerns separate; folding claims into
`AGENTS.md` is there if you'd rather keep the instructions and the claims about them in one file.
### When *not* to anchor a file

"Any file can be a hub" doesn't mean every file should be. Pitch and marketing prose — a
`README.md` especially — is a poor fit:

- **The claims are coarse.** A README describes behavior in broad strokes that span many symbols,
so an anchor either covers a near-whole-file span or trips on incidental edits — the
over-anchoring trap from [Choosing granularity](#choosing-granularity).
- **It usually duplicates hub prose.** The invariants a README restates should already be claimed
in the hubs anchored to the real code; anchoring the README just gives you a second copy to keep
honest.
- **On GitHub, frontmatter renders as a table.** GitHub displays a Markdown file's YAML frontmatter
as a metadata table at the top of the rendered page, so an `anchors:` block would sit above your
pitch on the repo's front page.

Anchor the code, and let the README link to the docs instead.

See also: [CI integration](./ci-integration.md) · [Examples](../examples.md).
6 changes: 4 additions & 2 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ hubs = ["hubs/*.md", "docs/hubs/*.md", "**/_hub.md"]
```

Any file a glob matches is treated as a hub if it parses as one (frontmatter `anchors:` block +
markdown body), so the list can also pull in files that aren't named like hubs — for example
`AGENTS.md` (see [Claims in AGENTS.md / CLAUDE.md](../guides/authoring-hubs.md#claims-in-agentsmd--claudemd)).
markdown body), so claims can live in *any* file — not just files under `hubs/`. The list can pull
in files that aren't named like hubs, e.g. `AGENTS.md` or `CLAUDE.md` (see
[Where claims can live](../guides/authoring-hubs.md#where-claims-can-live) for the recommended
layout and the trade-offs).

> **`surf new` uses only the first glob.** When scaffolding a hub it writes into the directory
> derived from the first pattern (e.g. `docs/hubs/*.md` → `docs/hubs/`); the other patterns are
Expand Down
Loading