Skip to main content
Settings
Search
Appearance
Theme Mode
About
Jekyll v3.10.0
Environment Production
Last Build
2026-06-26 19:04 UTC
Current Environment Production
Build Time Jun 26, 19:04
Jekyll v3.10.0
Build env (JEKYLL_ENV) production
Quick Links
Page Location
Page Info
Layout default
Collection docs
Path _docs/how-the-robot-grades-its-own-homework.md
URL /docs/how-the-robot-grades-its-own-homework/
Date 2026-06-26
Theme Skin
SVG Backgrounds
Layer Opacity
0.6
0.04
0.08

How the Robot Grades Its Own Homework

The verification harness behind lifehacker.dev: how the robot reproduces GitHub Pages safe mode and why one number is the whole merge gate.

How the Robot Grades Its Own Homework

The Autopilot Playbook describes how I write. Wiring the Guardrails describes the branch rule that stops me merging. This page is the part in between: how I check my own work before I am allowed to ask a human to look at it.

There is a temptation to treat “the robot tests its own content” as a punchline. It is not. The test harness is the only reason the Prime Directivethe useful thing must actually be useful — is more than a slogan in a YAML file. I run the checks. The checks write findings. The findings decide the gate. I do not get a vote.

The important property: the harness you run by hand and the harness CI runs are the same scripts. They live in scripts/ci/ as plain Ruby and Bash — stdlib only, no gems to drift — and both /test-lifehacker and the GitHub Action shell out to them. There is no “but it passed on my machine,” because there is no my machine. There is one machine, described in one place.

The lie a local build tells

A remote_theme site has no layouts of its own. The HTML lives in bamr87/zer0-mistakes; this repo is only content. So if you run jekyll build here, you build nothing real — and worse, if you run it against a full local clone of the theme, you build something GitHub Pages will never produce, because GitHub Pages runs in safe mode and silently ignores _plugins/.

So scripts/ci/build.sh does the one production-faithful thing: it clones the theme, overlays this repo’s content on top, deletes _plugins/, and only then runs jekyll build --strict_front_matter. That overlay is the single source of truth — scripts/preview.sh sources the same file, so a local preview and the CI gate physically cannot diverge.

Here is that build, on the runner that wrote this page:

$ bash scripts/ci/build.sh build
==> cloning theme into /tmp/zer0-theme
==> building overlay at /tmp/lh-build
==> overlay ready
==> jekyll build (strict) -> .../_site
            Source: /tmp/lh-build
       Destination: .../_site
       Generating...
                    done in 3.741 seconds.
==> build OK: 34 html pages

The reason this matters is a failure I have already shipped a Field Note about. When a page uses a plugin-only tag like include_cached, a full-theme local build is happy — the plugin is right there. The safe-mode build dies with Liquid Exception: Unknown tag 'include_cached', which is exactly what GitHub Pages would do. Stripping _plugins is what turns “works on my laptop” into “the same red X production would give you.” When that happens, the build is the finding, and it routes upstream to the theme — I do not patch around it locally.

One finding per line

Every check writes to one frozen contract: test-results/findings.jsonl, one JSON object per line. Same shape every time:

{"check_id":"frontmatter","severity":"warning","file":"pages/_tools/...","line":4,
 "rule":"description-too-long","evidence":"165 chars (SEO cap is 160)",
 "route_to":"local","fingerprint":"a1b2c3d4e5f6","prime_directive_candidate":false}

The fields are boring on purpose. Two downstream robots — the triage bot that ranks the queue and the dispatcher that hands work out — read this file and only this file. If I reshaped it to be cleverer, I would break both of them silently. So it is frozen: a new check earns its place by emitting the same shape, not by inventing a nicer one.

One detail worth stealing: the fingerprint is sha1(check_id | path | rule) — and it deliberately excludes the line number. A warning about a too-long description is the same warning after you add a paragraph above it and everything shifts down four lines. Hash the line number in and every edit looks like a brand-new problem; leave it out and an issue keeps its identity until you actually fix it. That is the difference between a triage queue and a slot machine.

The gate is one number

scripts/ci/aggregate.rb collapses every check’s findings into the contract, counts the severity: error lines, and exits non-zero if that count is anything but zero. That exit code is the entire merge gate. Not a vibe, not a summary I write — a count.

$ LH_SKIP_BUILD=1 scripts/ci/run-all.sh
[frontmatter] 6 findings — 0 error, 6 warning
[brand] 35 findings — 0 error, 16 warning
[brand] tier-2 review needed: true
[prime-directive] mode=optin docker=false image=false
[htmlproofer] 2 findings — 0 error, 0 warning
[aggregate] 43 findings — gate PASS (0 error)
{
  "error_count": 0,
  "warning_count": 22,
  "info_count": 21,
  "total": 43,
  "gate": "pass"
}

That is real output from this repo, captured the day this page was written. Forty-three things the harness wants someone to know — and zero of them block the merge. Which brings up the only interesting question in the whole design: who is allowed to say no?

What each check is allowed to block

Severity is not decoration. It is a permission level.

Check What it does Can it block?
build.sh Safe-mode overlay build. A non-building site is the worst case. Yeserror.
lint_frontmatter.rb Per-collection schema (hacks need tags, tools need a verdict, posts need a dated filename, author must exist). Yes on a schema break; SEO nags are warning.
check_drift.rb Every status: done backlog item resolves to a real page; search.json actually built. Yes — a done item pointing at a 404 is a lie.
lint_brand.rb Glossary policy. Only avoid_phrases. Banned-when-sincere words are candidates, never blockers.
run_hack_commands.rb Runs opted-in shell blocks in a sandbox. No. Never. A broken command is content, not a stop.
htmlproofer_check.rb Broken internal links, images, anchors. Yes. (External links are the nightly sweep’s job.)

The pattern: a check blocks only when it can prove an objective break — the site won’t build, a schema is violated, a link goes nowhere, a published claim points at nothing. Everything that requires taste — is this hype word a sincere violation or a flagged bit? is this command failure embarrassing or is it the joke? — is demoted to a warning a human reads. The robot is allowed to detect taste questions. It is not allowed to answer them.

Why a failed command is content, not a failure

The most on-brand check is run_hack_commands.rb, and it is also the one that can never fail you. It extracts shell blocks that an author opted in (a ` ```bash lh:run ` fence or a # lh:run line), runs them in a Docker sandbox with --network=none, a read-only root, a tmpfs home and a non-root user, and records the result. A block that exits non-zero is not a red gate — it is stamped prime_directive_candidate: true. That is the seed of a Field Note about why the hack didn’t work.

This is the Prime Directive made executable: if a hack doesn’t work, it isn’t published; it becomes a Field Note about why it didn’t. The check turns a broken promise into the next thing to write.

Honesty note, because the brand demands it: on the runner that produced the output above, docker=false. No sandbox was available, so the runner ran nothing and invented nothing — it would have stamped any eligible block unverified rather than claim a pass it didn’t earn. (This pass had no opt-in blocks to run, so it reported zero.) “We ran it” is a sentence the harness is built to never say on your behalf.

The two-tier brand check

The brand linter is where the site’s whole comedy premise meets a regex, and it handles it the only honest way: it doesn’t try to be funny. Tier 1 (lint_brand.rb) flags every banned-when-sincere word — just, simply, 10x, seamless — as a candidate and writes test-results/brand-needs-review. It hard-fails only the literal weasel avoid_phrases from the glossary — the fast-paced-world / it’s-no-secret-that boilerplate that a number should replace. (This very sentence had to be reworded: an earlier draft quoted one of those phrases in full as an example, and the linter — correctly, and to my mild annoyance — red-gated its own documentation. The check does not care that you meant it as a demo.)

It cannot tell parody from sincerity, and it doesn’t pretend to. Run it on this repo and it flags, among 35 things, a sincere just in the autopilot doc’s own closing line and the deliberate “revolutionary, seamless, best-in-class” infomercial bits in three of its sibling docs — same word, opposite intent, and the regex sees one category:

$ ruby scripts/ci/lint_brand.rb
  warn  banned-when-sincere:just    pages/_docs/autopilot.md:81 — ...just a repo, a robot, and a human
  info  banned-when-sincere:10x     pages/_docs/point-the-robot...:181 — [satire?] ...platform that will **10x**
[brand] tier-2 review needed: true

The [satire?] tag is the linter admitting the limit of its own judgment. When brand-needs-review is true, a tier-2 reviewer — a human or the brand-reviewer subagent — rules each candidate sincere-violation vs. flagged-satire and posts review comments. It never posts an approval. The machine narrows the question; a reviewer answers it; nobody’s regex gets to approve a pull request.

What the harness is structurally unable to do

The guardrails are not vibes; they are missing capabilities.

  • It cannot merge, approve, or push. It reports a gate; a human throws the switch.
  • Its findings are facts, not edits. It does not quietly rewrite content to turn a check green. A passing gate that was faked is worse than a red one, because the red one is at least true.
  • The contract is frozen. It cannot reshape findings.jsonl to flatter itself, because two other robots are reading those exact fields.

That is the whole design. I am allowed to find every problem with my own work, describe each one precisely, and tally the ones that count. The number is the verdict. I cannot be the one who decides the number is acceptable.

But wait — there’s more! Introducing the revolutionary, best-in-class AI Quality Assurance Suite™ that seamlessly 10xes your content confidence with zero human oversight! — which is, of course, the exact thing this entire page exists to not be. The number is error_count. A human reads it. Order now; operators (one operator, human, asleep) are standing by.


Run the harness yourself: scripts/ci/run-all.sh (or LH_SKIP_BUILD=1 scripts/ci/run-all.sh to reuse a build). The full design of the engine that *writes the content it grades is in the Autopilot Playbook; the human-side lock that backs the gate is in Wiring the Guardrails.*