rebilder
v0.2.0
Published
The Rebilder CLI. Scores a URL against the Agent Readability Spec (ARS 0.2) from your machine, prints what an agent actually receives, and exits on a grade threshold. Zero telemetry: it never uploads a scan, a URL, a hostname, or a result.
Maintainers
Readme
rebilder
The Agent Readability Score, from your machine.
rebilder check <url> fetches a page the way an agent would, fetches it again
the way a browser would, and scores the result against
the Agent Readability Spec (ARS 0.2). It tells
you what a machine caller actually receives from your site, what that costs in
bytes, and which of the 22 checks you are losing points on.
ARS measures format and retrievability. It does not measure whether the facts are true, or whether any assistant cites this page.
npx rebilder check https://example.com/products/thingZero telemetry. This CLI never uploads a scan, a URL, a hostname, or a result — see the contract below.
Commands
rebilder check <url...> --format pretty|json|markdown|junit --fail-on <grade>
--out <file> --allow-private --allow-http --concurrency <n>
rebilder diff <url> # agent view vs browser view
rebilder init # gateway config scaffold for the detected framework
rebilder badge <domain> # embed snippetExit codes
A stable contract. Documented here, and never renumbered.
| Code | Meaning | | ---: | --------------------------------------------------------------------------- | | 0 | Completed; threshold met, or no threshold set | | 1 | Completed; threshold not met | | 2 | Usage error | | 3 | Probe failed / target unreachable — infrastructure, not a score failure | | 4 | Blocked by policy (SSRF guard, robots opt-out, probe budget spent) |
The 1-vs-3 split is load-bearing. 1 is a fact about your site. 3 is DNS,
a timeout, a 502, or a captive portal in your CI runner. If a flaky network
exited 1, the first red build nobody could explain would get the check deleted
— and a deleted check protects nothing. So an outcome that never produced a
score can never produce 1, and there is a test that sweeps every unscored
reason and every probe rejection to prove it.
4 is the other thing that is not a score failure: we chose not to fetch, or
were told not to. It is separate from 3 because 4 is deterministic and 3 is
not — re-running fixes a 3 and never fixes a 4.
--fail-on defaults to nothing. Plain rebilder check <url> reports and
exits 0. A tool that fails a build by default is a tool that gets || true
appended on day one.
--fail-on C means C or better passes; a worse grade exits 1.
When one invocation covers several URLs, the codes collapse 2 > 4 > 3 > 1 > 0: a mistake in the command beats everything, a deterministic refusal beats a transient failure, and anything that stopped us getting a score beats a score we got and disliked.
In CI
- run: npx rebilder check https://example.com/products/thing --fail-on B --format junit --out ars.xml3 and 4 will still fail the step. If you want infrastructure trouble to be
non-blocking while a grade regression is not, branch on the code:
npx rebilder check "$URL" --fail-on B; code=$?
case $code in
0) ;; # fine
1) echo "ARS grade regressed"; exit 1 ;; # your site
3|4) echo "probe did not complete ($code) — not failing the build" ;;
*) exit $code ;;
esacrebilder check
Per URL it makes two requests — an agent probe and a browser control that
differ in the Accept header and in nothing else — plus /robots.txt,
/llms.txt and /.well-known/ucp per origin. Nothing else is requested; there
is no guessing at /sitemap.xml or /mcp. robots.txt is fetched first and
obeyed.
Same User-Agent on both probes, because that is what makes the parity check
meaningful and what stops a correctly installed gateway — which negotiates on
Accept, not on User-Agent — from being scored as a cloaker.
Before: the page as raw HTML
https://basecamp-supply.example/products/alpine-trail-pack-28l
────────────────────────────────────────────────────────────────
D Partial 59/100
63 of the 100 points are measured, 37 heuristic
page kind: product (high confidence, heuristic)
Retrievability 28/28 heuristic
✔ Reachable on the agent path 11/11 measured
✔ Robots policy for assistants 10/10 measured
✔ Core facts present without JavaScript 7/7 heuristic
Machine representation 0/18 heuristic
✘ Machine representation on Accept negotiation 0/9 measured
✘ Declared alternate representations 0/3 measured
✘ Vary: Accept declared correctly 0/3 measured
✘ Same substance across representations 0/3 heuristic
Fact coverage 5/20 heuristic
◐ Core facts for this page kind 4/14 heuristic
◐ Context cost relative to this page kind 1/6 measured
Fact position 9/10 heuristic
◐ Byte offset of the first core fact 4/5 heuristic
✔ Core facts inside the front window 5/5 heuristic
Structured data 6/6 heuristic
✔ Structured data present and parsing 2/2 measured
✔ Recognised type with its required properties 3/3 measured
✔ Structured data agrees with visible text 1/1 heuristic
Contract & discovery 5/10 measured
✔ Canonical URL present and self-consistent 4/4 measured
✘ Cache validators 0/2 measured
✘ llms.txt present and spec-shaped 0/1 measured
✔ Sitemap declared in robots.txt 1/1 measured
✘ Declared machine endpoint 0/2 measured
Evidence density 6/8 heuristic
✔ Quantities an answer can quote 4/4 measured
✔ Labelled term and value pairs 2/2 heuristic
✘ Comparable rows 0/2 measured
What an agent pays
HTML 91,226 B ≈22,806 tokens (est.)
Negotiated — —
First core fact at byte 632
Core facts found 4/4 (100%) for a product page — heuristic
Fixes, most points first
+15 Serve a machine representation on Accept negotiation (config)
Return Markdown, plain text or JSON with a matching Content-Type when t…
+10 State the missing core facts for this page kind (template)
Coverage is scored against the fact set this kind of page is expected t…
+5 Cut the bytes an agent has to read (engineering)
Context cost is scored against what a page of this kind reasonably cost…
+3 Declare the alternate representation (config)
Add `Link: <…>; rel="alternate"; type="text/markdown"` or a typed `<lin…
+2 Add cache headers an agent can act on (config)
Send a Cache-Control a cache can use plus an ETag or Last-Modified. Rep…
+2 Put the options side by side (template)
Sizes, tiers, models, plans — one row each, in a table with at least tw…
+2 Declare a machine endpoint (engineering)
A feed, an OpenAPI document, /.well-known/ucp — anything an agent can c…
+1 Publish /llms.txt (config)
An H1 title, a short summary, and links to the pages that matter. It is…
+1 Move the core facts to the front (template)
A caller that reads the first few kilobytes should already have the fac…
Policy: robots.txt ok · assistant allow · training allow (neutral) · sitemap declared
ARS 0.2.0 · ruleset 18f8b6ce4a0d · corpus 15ec5c9c0966 · evidence b774a1e59433 · vantage public
ARS measures format and retrievability. It does not measure whether the
facts are true, or whether any assistant cites this page.After: the same page with content negotiation
https://basecamp-supply.example/products/alpine-trail-pack-28l
────────────────────────────────────────────────────────────────
A Agent-native 93/100
63 of the 100 points are measured, 37 heuristic
page kind: product (high confidence, heuristic)
Retrievability 28/28 heuristic
✔ Reachable on the agent path 11/11 measured
✔ Robots policy for assistants 10/10 measured
✔ Core facts present without JavaScript 7/7 heuristic
Machine representation 15/18 heuristic
✔ Machine representation on Accept negotiation 9/9 measured
✘ Declared alternate representations 0/3 measured
✔ Vary: Accept declared correctly 3/3 measured
✔ Same substance across representations 3/3 heuristic
Fact coverage 20/20 heuristic
✔ Core facts for this page kind 14/14 heuristic
✔ Context cost relative to this page kind 6/6 measured
Fact position 10/10 heuristic
✔ Byte offset of the first core fact 5/5 heuristic
✔ Core facts inside the front window 5/5 heuristic
Structured data 6/6 heuristic
✔ Structured data present and parsing 2/2 measured
✔ Recognised type with its required properties 3/3 measured
✔ Structured data agrees with visible text 1/1 heuristic
Contract & discovery 7/10 measured
◐ Canonical URL present and self-consistent 3/4 measured
✔ Cache validators 2/2 measured
✔ llms.txt present and spec-shaped 1/1 measured
✔ Sitemap declared in robots.txt 1/1 measured
✘ Declared machine endpoint 0/2 measured
Evidence density 7/8 heuristic
◐ Quantities an answer can quote 3/4 measured
✔ Labelled term and value pairs 2/2 heuristic
✔ Comparable rows 2/2 measured
What an agent pays
HTML 91,226 B ≈22,806 tokens (est.)
Negotiated 1,490 B ≈372 tokens (est.)
Reduction 99%
First core fact at byte 0
Core facts found 4/4 (100%) for a product page — heuristic
Fixes, most points first
+3 Declare the alternate representation (config)
Add `Link: <…>; rel="alternate"; type="text/markdown"` or a typed `<lin…
+2 Declare a machine endpoint (engineering)
A feed, an OpenAPI document, /.well-known/ucp — anything an agent can c…
+1 Declare one canonical URL, in both representations (config)
Add an absolute `<link rel="canonical">` to the HTML and a `Link: <…>; …
+1 State the numbers in text (template)
Price, sizes, weights, hours, fees, lead times, dates — written out whe…
Policy: robots.txt ok · assistant allow · training allow (neutral) · sitemap declared
ARS 0.2.0 · ruleset 18f8b6ce4a0d · corpus 15ec5c9c0966 · evidence 1fe68289a133 · vantage public
ARS measures format and retrievability. It does not measure whether the
facts are true, or whether any assistant cites this page.Same product, same facts. 54 → 90, D → A, and the agent reads 1,490 bytes instead of 91,226.
Options
| Option | |
| ---------------------------------------- | ----------------------------------------------------------------------- |
| --format pretty\|json\|markdown\|junit | default pretty |
| --fail-on A\|B\|C\|D\|F | default none; C means C or better passes |
| --out <file> | write the same rendering to a file (stdout still prints) |
| --concurrency <n> | URLs in flight; default 1, max 8 |
| --allow-http | permit plain http targets |
| --allow-private | permit private, loopback and link-local targets; implies --allow-http |
--concurrency is capped at 8 because the probe's politeness limiter allows at
most 1 in-flight request per host and 2 globally regardless — a larger number
would be a promise we do not keep.
rebilder diff
What the agent request received, next to what the browser request received.
https://basecamp-supply.example/products/alpine-trail-pack-28l
────────────────────────────────────────────────────────────────
The two requests differ in the Accept header and in nothing else — same User-Agent, same everything (§3.3).
agent browser
Accept text/markdown;q=1.0, text/html;… text/html,application/xhtml+xml…
Status 200 200
Content-Type text/markdown; charset=utf-8 text/html; charset=utf-8
Bytes 1,490 B 91,226 B
Tokens ≈373 tokens (est.) ≈22,789 tokens (est.)
Redirects 0 0
Error — —
Substance parity [heuristic] 3/3
Compared on price, currency, availability and title only — never description or free text (§3.8).
• Compared facts: price, currency, availability, title [heuristic]
• Values: agree [heuristic]
Score: 93/100 A
ARS 0.2.0 · ruleset 18f8b6ce4a0d · corpus 15ec5c9c0966 · evidence 1fe68289a133 · vantage public
ARS measures format and retrievability. It does not measure whether the
facts are true, or whether any assistant cites this page.It shows structure and numbers, never an excerpt of the page. Dumping a
third party's content into a terminal, a CI log or an --out file is a
copyright problem the numbers do not have.
--fail-on is refused here rather than ignored: diff reports, it does not
grade, and silently dropping a flag someone put in a CI file is how a check that
appears to gate stops gating.
--format junit
One <testcase> per ARS check, one <testsuite> per URL, and the failure/error
split mirrors the exit-code contract: a check that fell short is a <failure>; a
page we never scored is an <error>.
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="rebilder.ars" tests="1" failures="0" errors="1">
<testsuite name="https://basecamp-supply.example/products/alpine-trail-pack-28l" tests="1" failures="0" errors="1">
<properties>
<property name="notice" value="ARS measures format and retrievability. It does not measure whether the facts are true, or whether any assistant cites this page."/>
<property name="url" value="https://basecamp-supply.example/products/alpine-trail-pack-28l"/>
<property name="generatedAt" value="2026-08-05T00:00:00.000Z"/>
<property name="tool" value="rebilder 0.2.0"/>
<property name="specVersion" value="0.2.0"/>
<property name="rulesetHash" value="18f8b6ce4a0df9a44e86bdb77f1098af4e6a9a8d9fd10e0df894649bb3eddf86"/>
<property name="corpusHash" value="15ec5c9c09665bc24174e86dbf037f8ea0433cf48c97eb5b2a720b8cfbc8684b"/>
<property name="evidenceHash" value="e1cc1ad1476117e7335e4b608e80b4a6e1e0072ce532dd6c9b5f4cce0b2f8b19"/>
<property name="measuredWeight" value="63"/>
<property name="heuristicWeight" value="37"/>
<property name="exitCode" value="3"/>
<property name="notAScoreFailure" value="probe failure — infrastructure"/>
</properties>
<testcase classname="https://basecamp-supply.example/products/alpine-trail-pack-28l" name="probe">
<error type="unscored" message="UNSCORED — unreachable (timeout)"/>
</testcase>
</testsuite>
</testsuites>Zero failures, one error, exit 3. The two signals agree.
With --fail-on, one extra test case per URL carries the threshold, so the gate
is a visible line in the report rather than something implied by the exit code.
Without it that case is absent, not passing.
rebilder init
Writes a @rebilder/gateway config scaffold for the framework it detects
(next, node, edge, shopify), and prints how it decided. --framework
overrides detection. Makes no network request and reads nothing outside the
working directory.
The scaffold is a skeleton with TODOs, not a working config: the resolvers
return your source of truth, and every substantive value on a rendered page
comes from those fields. The gateway never invents one.
rebilder badge
Badge for example.com
Markdown
[](https://rebilder.com/readable/example.com)
HTML
<a href="https://rebilder.com/readable/example.com"><img src="https://rebilder.com/badge/example.com.svg" alt="Agent Readability Score for example.com" height="20"></a>
A grade renders only for a verified opted-in domain. Every other domain gets a neutral “not rated” badge, served at 200 so it never shows as a broken image.
Verify by publishing a DNS TXT record `rebilder-verify=<token>`, or by serving `X-Rebilder-Path` from your origin where we can observe it.
The badge is served live from our data: you capture nothing and freeze nothing. It degrades to “unverified” after 30 days without a successful re-scan.
It always names its subject (`example.com · A · 94 · Mar 2026`) and it shows the ruleset hash alongside the spec version — two scores from different rulesets must not render identically.
No score ever comes from the query string. Removal from the index is free, self-serve, permanent, and never purchasable.
This command makes no network request. It builds the snippet locally, so we never learn which domain you asked about.Built locally — no network request, so we never learn which domain you asked about.
Zero telemetry
This is a contract, not a setting. There is no opt-out because there is nothing to opt out of.
The CLI never uploads a scan, a URL, a hostname, or a result. It has no
analytics, no error reporting, no version check, no "anonymous usage
statistics". rebilder.scans.source has no 'cli' value, because there is no
path by which a CLI run could produce a row.
The only outbound requests this binary makes are to the target URL you named, in
the request set described above. badge and init make none at all.
This matters most in exactly the case the tool is best at. A developer running
rebilder check --allow-private http://staging.internal.corp/checkoutinside their own CI must not thereby ship internal hostnames and paths to a
third party. --allow-private and the no-telemetry guarantee are only safe as a
pair, and neither is negotiable.
It is enforced three ways: the package declares exactly one runtime dependency
(@rebilder/agent-readability, which is the probe and the scorer); ESLint denies
node:http, node:https, node:net and fetch across src/; and a test reads
every source file to assert the same thing, because a lint rule can be disabled
inline and that inline disable is precisely the diff worth failing a test over.
--allow-private
Private, loopback and link-local targets are unlocked by a separate entry
point in @rebilder/agent-readability that throws at import time unless the
process was started with --allow-private, and refuses outright inside a hosted
runtime. It is not a boolean on a config object, because a boolean on a config
object is a boolean somebody eventually sets to true in production.
The permission is per invocation. It is never a default, never an environment variable, and never inherited.
Zero runtime dependencies
Hand-rolled ANSI (off when stdout is not a TTY, off whenever NO_COLOR is set,
on with FORCE_COLOR), Unicode box drawing with an ASCII fallback chosen by the
locale, and node:util's parseArgs. Node ≥ 20.11.
Colour and Unicode are decided separately because they fail in different places:
a pipe into a file is perfectly capable of holding │, and a LANG=C container
is not.
The one runtime dependency is @rebilder/agent-readability — the scorer and the
probe. There is no bundled second implementation of the standard.
Reading a score
Bands headline; the integer is secondary; there are no decimals.
| Grade | Range | Label | | ----- | ------ | -------------------- | | A | 90–100 | Agent-native | | B | 75–89 | Agent-friendly | | C | 60–74 | Readable with effort | | D | 40–59 | Partial | | F | 0–39 | Not extractable |
Three outcomes carry no letter grade at all:
- OPT-OUT — a deliberate, well-formed robots disallow for the assistant
audience. A choice, not a failure. Never ranked, never listed. Exit
4. - UNSCORED — blocked — a 403 or a challenge at the edge. The block is itself
the finding. Exit
3. - UNSCORED — unreachable / non-2xx / robots-unavailable /
robots-disallow-scanner / truncated-evidence. Exit
3, exceptrobots-disallow-scanner, which is4.
Blocking model-training crawlers while allowing assistants scores identically to a fully open site. Two conformance fixtures exist solely to prove it.
64 of the 100 points are measured; 36 are heuristic. Every check reports
which it is, in every format, always — a heuristic value never renders without
its label. Token counts are chars/4 and always carry ≈ and est..
--help
rebilder — the Agent Readability Score, from your machine.
USAGE
rebilder check <url...> [--format pretty|json|markdown|junit] [--fail-on A|B|C|D|F]
[--out <file>] [--allow-private] [--allow-http]
[--concurrency <n>]
rebilder diff <url> agent view vs browser view
rebilder init gateway config scaffold for the detected framework
rebilder badge <domain> embed snippet
rebilder help <command> detail for one command
rebilder --version
EXIT CODES (a stable contract; never renumbered)
0 completed; threshold met, or no threshold set
1 completed; threshold NOT met
2 usage error
3 probe failed / target unreachable — infrastructure, not a score failure
4 blocked by policy (SSRF guard, robots opt-out, probe budget spent)
The 1-vs-3 split is load-bearing. A flaky network exits 3, never 1: if an
unreachable host failed your build as a score failure, the check would be
deleted from CI within a week. --fail-on defaults to nothing, so plain
`rebilder check <url>` reports and exits 0.
ZERO TELEMETRY
This CLI ships zero telemetry. It never uploads a scan, a URL, a hostname,
or a result. The only requests it makes are to the target you name.
Spec: https://rebilder.com/spec/ars Crawler policy: https://rebilder.com/botsARS also cannot detect User-Agent-targeted cloaking, including cloaking
aimed at rebilder-ars itself, because both of our probes identify honestly and
we will not impersonate another operator's crawler. A site serving identical
fabricated substance to both requests scores as if it were truthful. That is a
limit of the method, stated next to the method.
The examples above are real
Every console block in this file is generated from a real fixture in
packages/agent-readability/conformance/ and labelled with its fixture id, and
tests/readme.test.ts regenerates and compares each one on every run. A
hand-edited block fails the build.
This rule exists because an earlier draft of this document shipped a mocked
47/100 alongside "+20 pts, D → B" — arithmetically impossible against its own
bands (47 + 20 = 67 = C) — and nobody caught it, because nothing could.
Regenerate with:
REBILDER_README_UPDATE=1 pnpm --filter rebilder testIt rewrites the blocks and then fails the run, so an update can never be mistaken for a pass.
Published
rebilder 0.1.0, Apache-2.0. Every
npx rebilder line in this README runs as written — the published tarball ships
a bundled dist/ and the rebilder bin, so there is no checkout and no build
step on your side.
The name is unscoped on purpose. npx rebilder check <url> is the thing people
are told to run, and npx @rebilder/cli would put a scope in the middle of the
one command that has to be memorable.
Note for contributors: workspace dependencies are rewritten, not bundled, at
pack time (docs/publishing.md). A workspace:* range that pnpm does not
rewrite ships as an uninstallable manifest, which is why releases go out through
the tagged workflow rather than a bare npm publish.
See also
@rebilder/agent-readability— the scorer (., pure) and the probe (./probe, server-only)@rebilder/gateway— the middlewarerebilder initscaffolds- The Agent Readability Spec
- Our crawler policy and opt-out
