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
20 changes: 18 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
---
summary: Operating instructions for AI agents; also dogfoods one sealed claim about the lint rule that governs this file.
anchors:
- claim: >
surf lint blocks when AGENTS.md carries a surf:hubs block that does not link the configured
hubs directory, or when that directory does not exist; without the block it stays silent.
at: surf-cli/src/lint.rs > lint_agents_pointer
hash: 938380798f7a
refs: []
---

# AGENTS.md

Guidance for AI coding agents working in this repo. (Humans: see
[`CONTRIBUTING.md`](./CONTRIBUTING.md) and [`docs/`](./docs/index.md).)

> This file is itself a hub (see `surf.toml`): the one sealed claim below anchors to the lint rule
> that polices this very file — a small bit of dogfooding. Everything else here is plain
> instruction, deliberately not anchored.

Surface is a deterministic gate that surfaces divergence between docs and code: you anchor a
sentence to the code it describes, and `surf check` blocks when that code's logic changes out
from under the prose. **This repo dogfoods Surface on its own source** — the gate runs on
Expand All @@ -24,8 +39,9 @@ codebase; reading all of them is wasteful context. They are split per module
covering the area you're working on.
<!-- /surf:hubs -->

`surf lint` enforces that this block stays — pointing at the hubs directory, never duplicating
or enumerating individual hubs.
`surf lint` enforces that when this block is present it links the hubs directory and that the
directory exists; by convention it points at the directory rather than duplicating or enumerating
individual hubs (so agents search, not read everything).

Caveat (the tool's own honest limit): a green gate means *the anchored code hasn't changed
since last verified* — not that every sentence is true, and nothing about code no hub anchored.
Expand Down
37 changes: 37 additions & 0 deletions docs/dogfood-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,43 @@ did about it, the lesson.* Keep it honest; the failures are the interesting part

---

## 2026-06-17 — Making AGENTS.md a hub caught AGENTS.md lying about itself

**Context:** We documented that `AGENTS.md`/`CLAUDE.md` *can* double as a hub (any file the `hubs`
glob matches that parses as a hub counts), then went to actually wire it up here: added `AGENTS.md`
to the glob and sealed one claim anchored to `lint_agents_pointer` — the lint rule that polices
`AGENTS.md` itself.

**What happened:** the claim couldn't be written as the existing prose. `AGENTS.md` said:

> `surf lint` enforces that this block stays — pointing at the hubs directory, never duplicating
> or enumerating individual hubs.

But `lint_agents_pointer` only checks that the `surf:hubs` block *links the hubs directory and that
the directory exists* — it does **not** enforce non-enumeration (that's design convention, not code).
The prose had quietly overstated the tool. The discipline of writing a claim that must match a
specific symbol's actual behavior forced the correction: split the sentence into what lint enforces
(link + existence) vs. what's by convention.

```
surf verify "surf-cli/src/lint.rs > lint_agents_pointer" → updated AGENTS.md
surf check → all anchored spans match
```

**Why it's a good story:** the self-referential loop closed — `AGENTS.md` now carries a sealed
claim about the rule that governs `AGENTS.md`. And the mere act of making a sentence *sealable*
surfaced that the un-anchored version had drifted from the code. The claim didn't catch drift over
time; it caught an overstatement that already existed, because anchoring forces you to say exactly
what the symbol does.

**Lesson / open question:** "write it so it can be anchored" is itself a forcing function for
honest prose, separate from the gate ever going red. Open question: how much of an imperative
instructions file is genuinely anchorable? Here it was exactly one sentence — the rest is process,
deliberately left unanchored. Coverage is still the product (cf. the 2026-06-12 entry); over-anchoring
`AGENTS.md` would just invite rubber-stamping.

---

## 2026-06-12 — Instructions are advisory; the gate isn't (agent edition)

**Context:** Asked Claude to knock out the 0.6.1 quick wins (`#71`, `#67`). It changed `surf for`'s
Expand Down
38 changes: 38 additions & 0 deletions docs/guides/authoring-hubs.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,42 @@ When that block is present, `surf lint` checks it links the configured hubs dire
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.

## Claims in `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:

```toml
# surf.toml
hubs = ["hubs/*.md", "AGENTS.md"]
```

```markdown
---
anchors:
- claim: the CLI exits non-zero when the gate finds a diverged claim
at: surf-cli/src/main.rs > main
hash: # written by `surf verify`
---

# Agent instructions

...your normal AGENTS.md prose...
```

`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` — 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.

See also: [CI integration](./ci-integration.md) · [Examples](../examples.md).
15 changes: 15 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ hubs = ["hubs/*.md"]
Point the glob wherever your hubs live: keep them central (`hubs/*.md`) or co-locate them with code
(e.g. `["**/_hub.md"]`).

`hubs` is a list, so you can combine locations — the matches are unioned, then sorted and
de-duplicated, so overlapping globs are safe:

```toml
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)).

> **`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
> still linted and verified normally, they're just not where `new` writes.

## Languages

TypeScript (`.ts`, `.tsx`, `.mts`, `.cts`), JavaScript/JSX (`.js`, `.jsx`, `.mjs`, `.cjs`), Rust
Expand Down
2 changes: 1 addition & 1 deletion surf.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
hubs = ["hubs/*.md"]
hubs = ["hubs/*.md", "AGENTS.md"]
Loading