Repository navigation
fix: close the agent-readiness gaps the Kitesurf sweep surfaced - #354
Merged
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
aadhar-sh | e4d1de8 | Aug 14 2026, 02:50 PM |
Contributor
Wire-size diff vs merge base
Brotli q11, the quality the edge serves. Every number here is deterministic: Client assetsNo change. 19 files, 68.08 KiB Brotli total. PagesTotal Brotli: 494.22 KiB → 494.35 KiB (+0.13 KiB, +0.0%) across 48 files, 1 changed.
Worker bundle278.56 KiB → 278.57 KiB gzip (+0.01 KiB, +0.0%) Largest module deltas (raw bytes in bundle):
Dictionary deltas (
|
oddharsh
force-pushed
the
agent-readiness-fixes
branch
from
August 12, 2026 20:49
30e5807 to
f41b482
Compare
oddharsh
force-pushed
the
agent-readiness-fixes
branch
from
August 12, 2026 21:15
bddcc9c to
d6e7c42
Compare
oddharsh
force-pushed
the
agent-readiness-fixes
branch
from
August 12, 2026 21:48
d6e7c42 to
e1ccba3
Compare
oddharsh
force-pushed
the
agent-readiness-fixes
branch
from
August 12, 2026 22:53
e1ccba3 to
d81489b
Compare
oddharsh
force-pushed
the
agent-readiness-fixes
branch
from
August 13, 2026 11:38
d81489b to
735c424
Compare
…code
The homepage grid was twelve blank squares in Cloudflare's Kitesurf. Every
tile returned HTTP 200 and none of them decoded, because the tiles are AVIF
and that engine cannot read it. A request-level view calls this healthy,
which is why nothing here caught it.
lib/photo-grid.js names ONE url per tile on purpose: a <picture> there had
its JPEG fallback instantiated over and over as the cursor crossed the grid
(13 loads for one tile, measured 2026-08-11). That decision is untouched.
This adds a RECOVERY path instead, and the whole design turns on a fact
measured 2026-08-12: Kitesurf does fire `error` on the tile it cannot decode.
So one delegated listener parses the stem back out of the failing URL and
retries through /images/<stem>.jpg, which 301s to the hashed JPEG. It adds no
bytes to any tile, because nothing is shipped that a working browser would
not use. Capture phase, since `error` on an <img> does not bubble.
Verified against the live grid, same listener as shipped:
KITESURF before 12 tiles, 12 broken, 0 repaired
KITESURF after 12 tiles, 0 broken, 12 repaired
CHROME after 12 tiles, 0 broken, 0 repaired
The last row is the point. A browser that decodes AVIF never fires the event,
so the fast path costs exactly nothing.
The <noscript> twin gets <picture> instead, and that asymmetry is deliberate.
Inert markup cannot instantiate a fallback, so the churn that took <picture>
off the live tiles is impossible there, while a no-JS reader that cannot
decode AVIF has no script running to repair anything. /photos already proves
the mechanism: it still ships <picture>, and Kitesurf selected the JPEG for
all 158 tiles and broke none, because a well-behaved engine DECLINES a source
type it cannot decode rather than claiming it and failing.
…own twin /writing, /lens, /coffee and /around all carry flags.agents: true in site-manifest.json and all four served no twin. Found 2026-08-12 by probing the twin PER ROUTE rather than trusting the site-wide count, which is 44 and was never wrong. They were missed because every one of them renders from the Worker, so htmlFileFor() returns null and there is no HTML for the generator to read. CLAUDE.md already names the fix for that case: hand-author into www/md/, or render live where the data exists in another representation, or drop flags.agents rather than advertise a surface an agent cannot read. /writing takes a third shape and it is the better one for a LIST. A hand twin would go stale the next time a post is added, so the twin is generated from www/writing/posts.json, which is the same committed registry the page itself renders. That is the ordinary twin argument reaching one step further back than the HTML: still a pure function of committed bytes, still impossible to drift. It links each post to its .txt, so an agent following a row gets the prose with no Notepad chrome to strip. The other three are hand twins beside bot.md, security.md and terminal.md. Each states plainly that it describes rather than mirrors, and points at the JSON that already carries the changing part: /around/json and /around/changes.json for the crawl, /coffee/availability.json for the slots. Every fact in them was read off the live endpoints or the source rather than recalled. Three of them carry a warning an agent needs and the HTML only implies. Booking is a REQUEST and a successful POST means "asked", never "booked". Availability fails closed, so a 503 is the safe answer rather than a broken one. A crawl skip is a legitimate outcome rather than an error. All four are swept by the route oracle now, each with a load-bearing sentence as its marker rather than a title, so a twin that degrades into a stub fails. Markers are single-line on purpose: the first one spanned a wrapped line and never matched.
The <noscript> tile briefly carried <picture>. It is SAFE there (inert markup
instantiates nothing, and a no-JS AVIF browser would still select the AVIF
source), but safe is not the bar. It costs 195 bytes a tile against 130, and
it buys a client that has to be no-JS AND no-AVIF simultaneously, when the
engine this whole repair exists for runs JavaScript and takes the onerror
path in index.html without ever reading that block.
One url per tile, everywhere, which is what this file said in the first place.
The live path is unchanged and was never the concern. Measured at the network
layer on the real homepage, counting image requests rather than repairs:
CHROME (decodes avif) 12 avif, 0 jpg, 0 broken tiles
KITESURF(no avif) 12 avif, 24 jpg, 0 broken tiles
Zero JPEG bytes reach a browser that can decode the AVIF it is already being
sent. Kitesurf's 24 is 12 redirect hops plus 12 images, so three requests a
tile on the engine that is already failing. The alternative is a data-jpg
attribute on every tile, which would ship ~35 bytes of never-used markup to
everyone to save a hop for almost nobody. The hop is the cheaper half.
oddharsh
force-pushed
the
agent-readiness-fixes
branch
from
August 14, 2026 14:48
735c424 to
e4d1de8
Compare
oddharsh
added a commit
that referenced
this pull request
Aug 24, 2026
…26-08-14 `src/content/md/` holds seven hand-authored twins (/around, /bot, /coffee, /lens, /security, /terminal, /whoareyou). CLAUDE.md's key-files row, its layout table, `.assetsignore`'s comment and the generator's own header all said three, and all four still called the directory `public/md/`. The count could rot because the set is not a list anywhere in the code. `buildTwins` looks for `<path>.md` per SURFACE, so a page joins by someone dropping a file in: /terminal joined in #227 and /around, /coffee and /lens in #354, and nothing announced either. What the row now records is the mechanism rather than the membership, plus the fact that `checkTwinFacts()` pins only three of the seven, so the other four can drift from their page silently. Two neighbouring rows named prose that moved in the same #456 split: - `public/writing/` is `src/content/writing/`, and its render code is in `src/worker/` rather than `_worker.js`. - `public/index.md` is `src/content/index.md`. That path confusion has already cost something: gen-md-twins looked in `public/` after the move, silently stopped finding the hand twin, and overwrote it with a generated one 472 bytes shorter. The row records that now. `.assetsignore` also opened on "Files in `www/`" and called the deltas `www/ad/`. Its two paragraphs about `www/scripts` and the old dictionary location stay, since those are the record of why there is no entry at either path.
oddharsh
added a commit
that referenced
this pull request
Aug 24, 2026
…26-08-14 `src/content/md/` holds seven hand-authored twins (/around, /bot, /coffee, /lens, /security, /terminal, /whoareyou). CLAUDE.md's key-files row, its layout table, `.assetsignore`'s comment and the generator's own header all said three, and all four still called the directory `public/md/`. The count could rot because the set is not a list anywhere in the code. `buildTwins` looks for `<path>.md` per SURFACE, so a page joins by someone dropping a file in: /terminal joined in #227 and /around, /coffee and /lens in #354, and nothing announced either. What the row now records is the mechanism rather than the membership, plus the fact that `checkTwinFacts()` pins only three of the seven, so the other four can drift from their page silently. Two neighbouring rows named prose that moved in the same #456 split: - `public/writing/` is `src/content/writing/`, and its render code is in `src/worker/` rather than `_worker.js`. - `public/index.md` is `src/content/index.md`. That path confusion has already cost something: gen-md-twins looked in `public/` after the move, silently stopped finding the hand twin, and overwrote it with a generated one 472 bytes shorter. The row records that now. `.assetsignore` also opened on "Files in `www/`" and called the deltas `www/ad/`. Its two paragraphs about `www/scripts` and the old dictionary location stay, since those are the record of why there is no entry at either path.
oddharsh
added a commit
that referenced
this pull request
Aug 24, 2026
…#563) * docs: the runbooks still describe the pnpm tree and the www directory `pnpm exec` refuses on a bun tree outright (it reads package.json's `packageManager`), and there is no pnpm workspace left for `--filter` to read, so every pnpm command in these files fails rather than degrading. Fixed in the clone-and-verify recipe, cal/README.md, `checkpoints:check`, `bun why esbuild`, and three live instructions in CLAUDE.md (`deploy:direct`, `infra:apply`, the cal secrets block). Four claims contradicted a value something else checks: - MAINTENANCE.md's rollout recipe told you to type `pnpm exec wrangler versions upload` into the Workers Builds dashboard, where infra.json declares `bash .github/deploy-wrangler.sh versions upload` and check-infra.mjs exact-matches it. Following the recipe reddens `validate`. CLAUDE.md's gotcha 29 carried the same stale string, and infra.json's own prose said "both moved to `pnpm exec`" fifteen lines above the value that is the wrapper. - MAINTENANCE.md said the wrapper picks its invocation from the lockfile. It stopped doing that on 2026-08-20 and runs the pinned entry file under node, because wrangler does not support bun and the refusal is per-command. - CLAUDE.md said the ramp calls `pnpm exec wrangler`. Spawns go through `wranglerCommand()` in tools/lib/wrangler-bin.ts, which names no package manager. This one already cost a release: five tools were still spawning `pnpm exec` the day the tree became bun, and two of them swallowed the refusal and reported a build that measured nothing. - CLAUDE.md's root-files list named `pnpm-workspace.yaml` and `pnpm-lock.yaml`. The lockfile is `bun.lock` and package.json carries the four workspaces. `www/` survived in two places. design/DESIGN.md cited `www/index.html` and `www/garage/*` for the two canonical Luna surfaces, and src/content/README.md was a whole fossil of the old directory, titled `# www/`, describing `_worker.js` and `scripts/` and linking a MAINTENANCE.md that resolves to nothing from where it now sits. That file BUILDS to .build/public/README.md, so it has been serving at aadhar.sh/README.md the whole time. Gotchas 29 and 30 keep their pnpm, since they are the record of that era, and so does the note that one dashboard string has to suit a pnpm branch and a bun one. Where a fix landed inside those blocks the old state is kept and the newer move appended rather than overwritten. * docs(twins): the hand-twin set says three and has been seven since 2026-08-14 `src/content/md/` holds seven hand-authored twins (/around, /bot, /coffee, /lens, /security, /terminal, /whoareyou). CLAUDE.md's key-files row, its layout table, `.assetsignore`'s comment and the generator's own header all said three, and all four still called the directory `public/md/`. The count could rot because the set is not a list anywhere in the code. `buildTwins` looks for `<path>.md` per SURFACE, so a page joins by someone dropping a file in: /terminal joined in #227 and /around, /coffee and /lens in #354, and nothing announced either. What the row now records is the mechanism rather than the membership, plus the fact that `checkTwinFacts()` pins only three of the seven, so the other four can drift from their page silently. Two neighbouring rows named prose that moved in the same #456 split: - `public/writing/` is `src/content/writing/`, and its render code is in `src/worker/` rather than `_worker.js`. - `public/index.md` is `src/content/index.md`. That path confusion has already cost something: gen-md-twins looked in `public/` after the move, silently stopped finding the hand twin, and overwrote it with a generated one 472 bytes shorter. The row records that now. `.assetsignore` also opened on "Files in `www/`" and called the deltas `www/ad/`. Its two paragraphs about `www/scripts` and the old dictionary location stay, since those are the record of why there is no entry at either path. --------- Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #353, which found these. This fixes them.
1. The photo grid was twelve blank squares
Every tile returned HTTP 200 and none decoded, because the tiles are AVIF and Kitesurf cannot read it. A request-level view calls that healthy, which is why nothing here caught it.
lib/photo-grid.jsnames one URL per tile deliberately, and that decision is untouched. This adds a recovery path instead, and the design turns on a fact measured first: Kitesurf does fireerroron the tile it cannot decode. One delegated listener parses the stem back out of the failing URL and retries through/images/<stem>.jpg, which 301s to the hashed JPEG. No bytes are added to any tile.Verified against the live grid with the listener as shipped:
The last row is the point: a browser that decodes AVIF never fires the event, so the fast path costs nothing.
The
<noscript>twin gets<picture>instead, and the asymmetry is deliberate. Inert markup cannot instantiate a fallback, so the churn that took<picture>off the live tiles is impossible there, while a no-JS reader has no script running to repair anything.Worth knowing for future decisions:
<picture>works correctly in Kitesurf./photosstill ships it, and Kitesurf selected the JPEG for all 158 tiles and broke none, because a well-behaved engine declines a source type it cannot decode rather than claiming it. Gotcha 7's decode-failure warning does not apply to this engine.2. Four surfaces advertised a twin and served none
/writing,/lens,/coffeeand/aroundall carryflags.agents: trueand had no Markdown twin. All four render from the Worker, sohtmlFileFor()returns null and there is nothing for the generator to read./writingis generated fromwww/writing/posts.json, the same committed registry the page renders. A hand twin would go stale on the next post; this is the ordinary twin argument reaching one step further back than the HTML. Each row links to the post's.txt, so an agent gets prose with no Notepad chrome.The other three are hand twins beside
bot.mdandterminal.md, each stating plainly that it describes rather than mirrors, and pointing at the JSON that carries the changing part. Every fact was read off the live endpoints rather than recalled.Three carry a warning an agent needs that the HTML only implies: booking is a request, so a successful POST means "asked" and never "booked"; availability fails closed, so a 503 is the safe answer; a crawl skip is a legitimate outcome.
All four are swept by the route oracle now (134 routes), each with a load-bearing sentence as its marker rather than a title.
3. WebMCP, investigated rather than fixed
Not actionable here, but it resolves the open unknown from #353.
<script src="/.webmcp/bridge.js">, no request for it. The browser tool catalog is simply absent on the agent browser.document.modelContextcarrying_listenersand_ontoolchangeand no methods.That second one is the same shape as the
navigationstub that brokenav.js, which makes it a bug class rather than a one-off: this engine ships stub objects for APIs it does not implement, soif (window.X)is unsafe by default andif (typeof X.method === "function")is the only reliable form.I swept the client scripts for other instances.
tooltip.jstouchesTemporalbehind a baretypeofcheck but wraps both call sites intry/catchwith aDatefallback, so a partial implementation degrades correctly. Everything else is either our own modules or already checks the method.navigationwas the only unguarded one and #353 fixes it.249/249 contract tests, typecheck clean, route oracle 134/134, page contracts clean.