Skip to content

Give loom the surfaces it was grading other libraries on - #4

Merged
houko merged 1 commit into
mainfrom
feat/agent-channels
Sep 20, 2026
Merged

houko merged 1 commit into
mainfrom
feat/agent-channels

Conversation

@houko

@houko houko commented Sep 20, 2026

Copy link
Copy Markdown
Member

lib/sources.ts ranks five component libraries by how much of each one a machine can reach unattended. Three publish an llms.txt, one runs an MCP server, three install from a CLI. Everything loom knew about its own blocks, themes, tokens and exports was reachable only by a person clicking buttons in a browser, which put loom last on the list it wrote.

This adds the three surfaces it was grading others on. No dependency was added for any of it — the JSON-RPC framing, the llms.txt builder and the ZIP writer are all in-repo.

MCP server

npm run mcp, hand-written JSON-RPC 2.0 over stdio. Driven for real:

$ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize",...}' \
    '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
    '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
    '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"sources_for_block","arguments":{"block":"nav"}}}' \
  | npm run --silent mcp

initialize            -> 2025-11-25  {name: loom, title: loom, version: 0.1.0}
tools/list            -> list_component_sources, sources_for_block, list_blocks, list_themes, theme_tokens
sources_for_block nav -> 3  [beUI, shadcn/ui, Rare UI]

Four messages in, three responses out — the notification is correctly unanswered.

Every answer is read from lib/plan.ts, lib/themes.ts and lib/sources.ts at call time rather than copied, so a tool cannot go on answering confidently after a table changes. A test calls every tool the server advertises, so a name added without a handler fails there rather than in front of an agent.

The two answers that are not lookups got the most care:

  • an unrecognised block returns an empty list with an explanation, never a fabricated row
  • theme_tokens on an unknown name reports which theme it actually returned. themeVars() falls back to forest, and an agent handed those values unlabelled pastes a green button in believing it asked for terminal.

Protocol revision 2025-11-25, chosen deliberately: /specification points at 2026-07-28, but that revision removes the initialize handshake, and the handshake is what Claude Code and Codex open with. initialize echoes back any of 2025-11-25 / 2025-06-18 / 2025-03-26 / 2024-11-05 a client asks for.

To use it: claude mcp add loom -- npm run --silent mcp from a checkout.

/llms.txt

Generated per request from the modules that serve the app, so it cannot drift.

$ curl -s -o /dev/null -w '%{http_code} %{content_type} %{size_download}\n' localhost:3118/llms.txt
200 text/plain; charset=utf-8 8937

llmstxt.org allows links only inside H2 sections, so blocks, themes, tokens, exports and sources are prose — inventing a /docs/blocks link to satisfy the shape would send an agent to a 404. Other libraries' endpoints appear as code spans, so every followable url points at the deployment the file came from. ## Optional is last, since the spec defines that name as the section to skip under context pressure.

The one hand-written list in that file went stale on its first opportunity. bundle.zip shipped in this same change and the export list still said five formats — exactly the failure the rest of the file is built to avoid. Rather than reach from a pure builder into a React component to recover six strings, the test now parses the download attributes out of app/page.tsx and fails when the two disagree. Written twice, checked once.

bundle.zip

A sixth export carrying all five artifacts plus a CLAUDE.md holding @AGENTS.md — closing a gap the generated brief admits in its own first paragraph, since Claude Code reads CLAUDE.md and not AGENTS.md.

Hand-written STORE zip. Verified with the system unzip, not only against its own reader, on a page generated from a Chinese prompt:

$ unzip -t bundle.zip
    testing: site-warm.html           OK
    testing: Site.tsx                 OK
    testing: spec.json                OK
    testing: site.registry.json       OK
    testing: AGENTS.md                OK
    testing: CLAUDE.md                OK
No errors detected in compressed data of bundle.zip.

$ unzip -p bundle.zip CLAUDE.md
@AGENTS.md

Sizes are measured on UTF-8 bytes rather than string length, so a page of CJK copy does not produce an archive that opens on the author's machine and fails on someone else's — there is a test that states that bug directly. The DOS timestamp is fixed, so two exports of the same page are byte-identical, and the test file parses the central directory with its own reader and recomputes CRCs with an independent implementation rather than importing the writer's table.

Verification

npx tsc --noEmit     exit 0
npx vitest run       16 files, 250 tests, all passing
npm run build        compiled successfully, /llms.txt in the route table
npm run mcp          real stdio handshake, 5 tools listed and called
unzip -t bundle.zip  6 files, no errors

End to end through the real editor with Playwright: six export buttons render, bundle.zip downloads, and the archive above is what came out of it.

51 new tests, every one mutation-verified — the implementing agents report the specific mutations tried, including several that stayed green on the first pass and led to a stronger test rather than being quietly dropped.

Also fixed

docs/CONTRIBUTING.zh.md was missing the "Adding a component source" section added in #3, had no "Adding an MCP tool" section, and carried a test count three changes out of date. All three fixed.

Still not in scope

npm run dev under Next 16 writes an untracked AGENTS.md and CLAUDE.md at the repo root. Next reports it is disabled with agentRules: false in next.config. Not changed here — it is a config decision, and this branch is already carrying three features.

lib/sources.ts ranks five component libraries by how much of each one a machine can reach unattended. Three publish an llms.txt, one runs an MCP server, three install from a CLI. Everything loom knew about its own blocks, themes, tokens and exports was reachable only by a person clicking buttons in a browser, which put loom last on the list it wrote. This adds the three surfaces it was grading others on. No dependency was added for any of it.

An MCP server, `npm run mcp`, over hand-written JSON-RPC on stdio. Five read-only tools: the component-source table, the sources for a given block, the 13 blocks with their variants, the six themes, and a theme's 17 tokens. Every answer is read from lib/plan.ts, lib/themes.ts and lib/sources.ts at call time rather than copied, so a tool cannot go on answering confidently after the table changes. A test calls every tool the server advertises, so a name added without a handler fails there instead of in front of an agent.

The two answers that are not lookups are the ones worth care. An unrecognised block returns an empty list with an explanation rather than a fabricated row, and `theme_tokens` on an unknown name says which theme it actually returned — `themeVars()` falls back to forest, and an agent handed those values unlabelled pastes a green button in believing it asked for terminal.

`/llms.txt`, generated per request from the same modules that serve the app. llmstxt.org allows links only inside H2 sections, so the blocks, themes, tokens, exports and sources are prose — inventing a /docs/blocks link to satisfy the shape would send an agent to a 404. Other libraries' endpoints appear as code spans, so every followable url points at the deployment the file came from.

The export list is the one thing in that file written by hand rather than derived, and it went stale on its first opportunity: bundle.zip shipped in this same change and the list still said five formats. Rather than reach from a pure builder into a React component to recover six strings, the test now reads the `download` attributes out of app/page.tsx and fails when the two disagree. Written twice, checked once.

A sixth export, `bundle.zip`, carrying all five artifacts plus a `CLAUDE.md` holding `@AGENTS.md` — closing a gap the generated brief admits in its own first paragraph, since Claude Code reads CLAUDE.md and not AGENTS.md. The archive is a hand-written STORE zip: table-driven CRC-32, sizes measured on UTF-8 bytes rather than string length so a page of CJK copy does not produce an archive that opens locally and fails elsewhere, the UTF-8 filename flag set, and a fixed DOS timestamp so two exports of the same page are byte-identical. Verified with the system unzip, not only against its own reader.

docs/CONTRIBUTING.zh.md was missing the component-source section added last change as well as the new MCP one, and carried a test count three changes out of date. Both fixed.
@houko
houko merged commit f6c4a54 into main Sep 20, 2026
1 check passed
@houko
houko deleted the feat/agent-channels branch September 20, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant