From f850c67d4d401fbcdc32a86acbacedbaa13b5c5c Mon Sep 17 00:00:00 2001 From: Connorrmcd6 Date: Wed, 17 Jun 2026 21:17:48 +0200 Subject: [PATCH 1/2] docs: document multi-location hubs and AGENTS.md/CLAUDE.md claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hubs glob is a list, and any matched file that parses as a hub counts — including AGENTS.md/CLAUDE.md. Surface both possibilities so users can decide, rather than implying hubs must live in hubs/. configuration.md gains a multi-glob example (noting the union/dedup and the surf-new-uses-first-glob caveat). authoring-hubs.md gains a "Claims in AGENTS.md / CLAUDE.md" section presented as a deliberate trade-off, keeping the existing keep-them-separate default intact. Co-Authored-By: Claude Opus 4.8 --- docs/guides/authoring-hubs.md | 38 +++++++++++++++++++++++++++++++++ docs/reference/configuration.md | 15 +++++++++++++ 2 files changed, 53 insertions(+) diff --git a/docs/guides/authoring-hubs.md b/docs/guides/authoring-hubs.md index f51aa1d..ea780eb 100644 --- a/docs/guides/authoring-hubs.md +++ b/docs/guides/authoring-hubs.md @@ -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 ` 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). diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 4f43b7a..b949801 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -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 From d5f920b8d4f73933eb36cd0106588a1cb965146c Mon Sep 17 00:00:00 2001 From: Connorrmcd6 Date: Wed, 17 Jun 2026 21:22:47 +0200 Subject: [PATCH 2/2] chore: dogfood AGENTS.md as a hub with one sealed claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AGENTS.md to the hubs glob and seal a single claim anchored to lint_agents_pointer — the lint rule that governs AGENTS.md itself. Writing a sealable claim surfaced that the existing prose overstated the rule (it enforces link+existence, not non-enumeration), so the prose is corrected to match the symbol. Logged in the dogfood-log. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 20 ++++++++++++++++++-- docs/dogfood-log.md | 37 +++++++++++++++++++++++++++++++++++++ surf.toml | 2 +- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c6f065a..8875a66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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 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. diff --git a/docs/dogfood-log.md b/docs/dogfood-log.md index 9f4269d..d25e21d 100644 --- a/docs/dogfood-log.md +++ b/docs/dogfood-log.md @@ -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 diff --git a/surf.toml b/surf.toml index 74c80ea..65f550a 100644 --- a/surf.toml +++ b/surf.toml @@ -1 +1 @@ -hubs = ["hubs/*.md"] +hubs = ["hubs/*.md", "AGENTS.md"]