For new contributors: this issue is self-contained — it explains the problem, where the code lives, how to build/test/dogfood, and concrete acceptance criteria. Start by reading the "Orientation" section, then pick a task from "Scope". Tasks are independent; #3 (docs) is a good first PR, #1–#2 (Rust) are the core fix.
TL;DR
Agents using the surface CLI write granular "claim-logs" — one claim per function, near-1:1 symbol→claim mapping, almost no prose. A hub is supposed to be the opposite: an onboarding doc that documents a system, prose-first, with claims that are coarse and span multiple anchor points (at: lists).
The fix is not "write better docs." It's that the CLI's in-loop signals reward the wrong behavior, and those signals are what actually teach agents (they read CLI output every iteration; they read prose docs once, if ever).
Why this happens (root cause)
| Signal |
Where |
What it teaches the agent |
surf suggest prints a "starter hub" listing every unanchored public symbol, one at: per symbol |
surf-cli/src/suggest.rs → print_human (~L166–189) |
"Write one claim per function." |
lint_under_coverage nags for every uncovered public function |
surf-cli/src/lint.rs → lint_under_coverage (~L376) |
"Drive the list to zero → one claim per function." |
The multi-site at: list (the actual tool for coarse, system-level claims) is documented only as a staleness footnote |
docs/guides/authoring-hubs.md (anchor grammar section) |
Never framed as the default; agents don't reach for it. |
AGENTS.md step 4: "Added public behavior? Add a hub claim for it — the under-coverage warning flags public functions with no claim." |
AGENTS.md |
Encodes the function-granular instinct in writing. |
There is currently no signal that rewards prose or claim consolidation. The only counter-pressure ("too many anchors in one hub") fires at the hub level, not the claim level.
What good vs. bad looks like
Bad (a "claim-log"): many claims, each anchoring a single symbol, restating what one function does, with a thin one-line # heading and no real prose. In this repo the maintainer points at hubs/cli-check.md as too thin — three single-site claims, ~5 lines of prose.
Good (an onboarding doc): a summary: that frames the whole system; a handful of coarse claims, several of which use multi-site at: lists to anchor one invariant across the 2–3 places it actually lives; and prose with structure (tables, ## sections, the "single most important distinction", a "Boundary" note on what the gate does not cover) that a human or agent reads to onboard. Anatomy of a good claim:
- claim: "trading commission is the only MULTI-LEVEL stream: it reads referral_graph for up to
three ancestors, is lifetime, skips self-edges, and pays REFERRAL_COMMISSION_RATES[tier][level]"
at:
- backend/.../referral-commission.service.ts > ReferralCommissionService > buildCommissionRecords
- packages/.../ReferralCommission.ts > REFERRAL_COMMISSION_RATES # one invariant, two sites
hash: 78e302b45972
One claim describes a behavior of the system and seals every span that behavior depends on — not one claim per function.
Scope (prioritized — lead with the CLI)
1. Reframe surf suggest output — highest leverage
surf-cli/src/suggest.rs → print_human. Today it prints a copy-pasteable starter hub with one at: per symbol, which agents treat as "claims to write."
- Add a preamble making clear these are undocumented symbols, not a list of claims — group related symbols into one system-level claim with a multi-site
at: list, prose first.
- Consider grouping suggestions by file/module, and/or emitting a single multi-anchor
at: skeleton instead of N 1:1 stubs, so the default output shape models a good hub.
- Keep
--format json shape unchanged (tooling depends on it; see json_shape_has_no_hash test).
2. Add symmetric, claim-level lint nudges (advisory, never blocking)
surf-cli/src/lint.rs. Mirror lint_coarse_span / lint_under_coverage with warnings that push toward consolidation:
- Thin-prose warning — a hub whose markdown body is mostly frontmatter / has a very low prose-to-claim ratio. A hub is an onboarding doc; flag when it isn't one.
- Granularity / "claim-log" smell — a hub with many single-site claims and near-1:1 symbol mapping → suggest consolidating into fewer multi-anchor claims.
- Add a new
Severity::Warn Finding for each; follow the existing lint_coarse_span pattern and add tests in surf-cli/tests/lint.rs.
3. Reframe the docs to match — good first PR
docs/guides/authoring-hubs.md: promote the multi-site at: list from staleness-footnote to the recommended default for system claims; add a short "a hub is an onboarding doc — prose first" section using the good/bad contrast above.
AGENTS.md: rewrite step 4 so the instinct is "extend an existing system claim's prose / add an anchor site" before "add a new granular claim."
- This repo's
docs/ is canonical (the docs site is generated from it) — see CONTRIBUTING "Docs source of truth".
4. (Optional) Prose-first surf new scaffold
surf-cli/src/new.rs — ship a template with ## How it works headings and one multi-anchor example claim, so a fresh surf new is shaped like a good hub.
Orientation for contributors
Layout (from CONTRIBUTING.md):
surf-core/ — pure parse/resolve/hash logic, no I/O.
surf-cli/ — the surf binary: commands + all I/O. All the code in this issue lives here, under surf-cli/src/{suggest,lint,new}.rs, with per-command tests in surf-cli/tests/.
Build & test:
cargo build
cargo test --all
cargo fmt --all
cargo clippy --all-targets -- -D warnings # CI fails on any warning
Dogfood your change (Surface governs its own hubs — this is the best way to see suggest/lint output for real):
cargo run -q -p surf-cli -- suggest "surf-cli/src/**/*.rs" # see #1's output
cargo run -q -p surf-cli -- lint # see #2's warnings
cargo run -q -p surf-cli -- check
If you change a symbol a hub anchors, check/lint will flag it — re-read the prose and surf verify "<at>" if it still holds (don't rubber-stamp). Add a [Unreleased] line to CHANGELOG.md for user-facing changes, and an entry to docs/dogfood-log.md if you hit a notable moment.
Acceptance criteria
Key insight
surf suggest + lint_under_coverage are the agent's de-facto teachers, and both currently teach "one claim per function." Fixing those two surfaces changes behavior more than any doc edit — which is why the CLI tasks (#1, #2) are prioritized over the docs task (#3).
🤖 Generated with Claude Code
TL;DR
Agents using the surface CLI write granular "claim-logs" — one claim per function, near-1:1 symbol→claim mapping, almost no prose. A hub is supposed to be the opposite: an onboarding doc that documents a system, prose-first, with claims that are coarse and span multiple anchor points (
at:lists).The fix is not "write better docs." It's that the CLI's in-loop signals reward the wrong behavior, and those signals are what actually teach agents (they read CLI output every iteration; they read prose docs once, if ever).
Why this happens (root cause)
surf suggestprints a "starter hub" listing every unanchored public symbol, oneat:per symbolsurf-cli/src/suggest.rs→print_human(~L166–189)lint_under_coveragenags for every uncovered public functionsurf-cli/src/lint.rs→lint_under_coverage(~L376)at:list (the actual tool for coarse, system-level claims) is documented only as a staleness footnotedocs/guides/authoring-hubs.md(anchor grammar section)AGENTS.mdstep 4: "Added public behavior? Add a hub claim for it — the under-coverage warning flags public functions with no claim."AGENTS.mdThere is currently no signal that rewards prose or claim consolidation. The only counter-pressure ("too many anchors in one hub") fires at the hub level, not the claim level.
What good vs. bad looks like
Bad (a "claim-log"): many claims, each anchoring a single symbol, restating what one function does, with a thin one-line
#heading and no real prose. In this repo the maintainer points athubs/cli-check.mdas too thin — three single-site claims, ~5 lines of prose.Good (an onboarding doc): a
summary:that frames the whole system; a handful of coarse claims, several of which use multi-siteat:lists to anchor one invariant across the 2–3 places it actually lives; and prose with structure (tables,## sections, the "single most important distinction", a "Boundary" note on what the gate does not cover) that a human or agent reads to onboard. Anatomy of a good claim:One claim describes a behavior of the system and seals every span that behavior depends on — not one claim per function.
Scope (prioritized — lead with the CLI)
1. Reframe
surf suggestoutput — highest leveragesurf-cli/src/suggest.rs→print_human. Today it prints a copy-pasteable starter hub with oneat:per symbol, which agents treat as "claims to write."at:list, prose first.at:skeleton instead of N 1:1 stubs, so the default output shape models a good hub.--format jsonshape unchanged (tooling depends on it; seejson_shape_has_no_hashtest).2. Add symmetric, claim-level lint nudges (advisory, never blocking)
surf-cli/src/lint.rs. Mirrorlint_coarse_span/lint_under_coveragewith warnings that push toward consolidation:Severity::WarnFindingfor each; follow the existinglint_coarse_spanpattern and add tests insurf-cli/tests/lint.rs.3. Reframe the docs to match — good first PR
docs/guides/authoring-hubs.md: promote the multi-siteat:list from staleness-footnote to the recommended default for system claims; add a short "a hub is an onboarding doc — prose first" section using the good/bad contrast above.AGENTS.md: rewrite step 4 so the instinct is "extend an existing system claim's prose / add an anchor site" before "add a new granular claim."docs/is canonical (the docs site is generated from it) — see CONTRIBUTING "Docs source of truth".4. (Optional) Prose-first
surf newscaffoldsurf-cli/src/new.rs— ship a template with## How it worksheadings and one multi-anchor example claim, so a freshsurf newis shaped like a good hub.Orientation for contributors
Layout (from
CONTRIBUTING.md):surf-core/— pure parse/resolve/hash logic, no I/O.surf-cli/— thesurfbinary: commands + all I/O. All the code in this issue lives here, undersurf-cli/src/{suggest,lint,new}.rs, with per-command tests insurf-cli/tests/.Build & test:
Dogfood your change (Surface governs its own hubs — this is the best way to see suggest/lint output for real):
If you change a symbol a hub anchors,
check/lintwill flag it — re-read the prose andsurf verify "<at>"if it still holds (don't rubber-stamp). Add a[Unreleased]line toCHANGELOG.mdfor user-facing changes, and an entry todocs/dogfood-log.mdif you hit a notable moment.Acceptance criteria
surf suggesthuman output no longer reads as "a list of claims to write" — it explicitly steers toward coarse, multi-anchor, prose-first hubs. JSON output unchanged.surf lintemits at least one new advisory warning that flags thin-prose and/or claim-log-shaped hubs, with tests insurf-cli/tests/lint.rs. Warnings stay exit-0 (non-blocking).docs/guides/authoring-hubs.mdandAGENTS.mdframe hubs as onboarding docs and multi-siteat:lists as the default for system claims.cargo test --all,cargo fmt --all,cargo clippy --all-targets -- -D warningsall clean.cargo run -p surf-cli -- check/lintstay green on this repo (re-verify intentionally changed anchors).Key insight
surf suggest+lint_under_coverageare the agent's de-facto teachers, and both currently teach "one claim per function." Fixing those two surfaces changes behavior more than any doc edit — which is why the CLI tasks (#1, #2) are prioritized over the docs task (#3).🤖 Generated with Claude Code