Port the public site onto the Go core, and fix a listener data race - #3
Merged
Conversation
The site pages were written against two generated JSON files that a Python step produced from the release artifacts. Those scripts are gone, so the pages need the same shapes from what remains: agents.lock.json for the catalog, and the GitHub Releases API for the downloads. catalog.ts gains `command`, `configPath` and `groups`, which the explorer and the activation demo already display, and now declares its types by importing explorer.ts rather than redeclaring them. Re-declaring was how a new protocol could reach the pages while the explorer still called it unsupported. release-channel.ts is new. It maps a release into the channel shape the download and security pages consume, and reports what the API cannot tell it as unknown instead of filling it in: `native_build: false` and `cleanroom: "not-recorded"` are the honest readings of "the release feed does not carry build provenance". Asserting `true` here would put a verification badge on the site that nothing checked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the public site work from main: the i18n routing layer with its hreflang and canonical handling, five English pages, the compatibility explorer, the activation demo that plays itself once scrolled into view, and the theme and locale controls. Every page that read a generated JSON file now reads the typed catalog and release-channel modules instead, which also removes eleven `as unknown as` and `as any` casts that only existed to give an untyped JSON import a shape. The security pages needed more than a rewire. They asserted that a specific macOS arm64 build had passed a cleanroom review — a claim the release feed cannot support. They now state what is checkable against the release page (channel, version, platform, digest) and say plainly that the build-provenance conclusions are recorded by the release process rather than asserted here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`npm run build` ended in `npm run validate`, but the validate script had been removed from package.json — so every build failed on a missing script after reporting success. Restored. validate-build.mjs used to re-hash each artifact in dist/downloads/ and compare it against the digest the release index claimed, which meant a page could not print a checksum the file did not have. That check has no subject now: the site links to GitHub Releases instead of hosting the artifacts, so there is no local file to hash. Removed, with a comment recording the cost — a wrong digest from the feed now reaches the page unchallenged — and what restoring an equivalent gate would take. release-index.json also came off the required-outputs list, since the site no longer emits it. The download-page tests skip, with a stated reason, when no release is published: the picker they drive is replaced by the unavailable notice, and this repository's release feed is currently empty. Skipping keeps them around to catch a regression once artifacts exist, where deleting them would let the download flow rot unnoticed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
exec.Cmd copies stdout and stderr on separate goroutines, so the listener passed to RunWithOutput was entered from both at once. The install runtime hands it a closure that redacts and forwards (internal/app/install.go), and that closure holds no lock — so this was a data race in production, not only under test. It shows up whenever a command writes to both streams, which npm does on every install. The lock lives in the runner rather than in each caller: "calls are serialised" is the contract a listener should be able to rely on. Both streamWriters share one mutex, since a mutex per writer would have the two goroutines taking different locks and entering the listener together anyway. TestOSRunnerSerialisesListenerAcrossStreams drives 50 interleaved writes to each stream through a listener that appends without locking, mirroring the production shape. Reverting the fix makes it fail with DATA RACE, which the previous tests only did by accident: they left stderr empty, so `-cover` adding a "GOCOVERDIR not set" warning was what exposed the race at all. That warning also polluted three assertions about captured output, so the helper now gets a scratch GOCOVERDIR of its own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
manifest_embed.go says a checked-in .keep makes the stage-0 shell buildable before Vite has produced dist. The file was never in the repository: two `dist/` rules excluded the directory itself, and excluding a directory stops git descending into it, so the `!frontend/dist/.keep` exception that was already sitting in the root .gitignore could never take effect. The consequence is that `go:embed all:frontend/dist` had nothing to embed in a fresh clone, and `go vet ./...` and `go build` failed on a missing embed pattern until someone happened to build the frontend first. Both rules now list their entries (`dist/*` plus the negation) instead of excluding the directory, and the root rule is anchored to `/dist/` so it stops matching at arbitrary depth. Build output stays ignored — verified that frontend/dist/index.html and site/dist/ still are. Co-Authored-By: Claude Fable 5 <noreply@anthropic.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.
Brings the public site work from
mainontorefactor/walis, adapted to this branch's data sources.refactor/waliswas branched before that work landed, so itssite/directory is file-for-file the 2026-07-29 version — none of the 23 new files existed here.Base is
refactor/walis, notmain, so this does not ask you to resolve the two parallel Go migrations. That decision stays with PR #2.The site port
The i18n routing layer, five English pages, the compatibility explorer, the self-playing activation demo, and the theme/locale controls.
mainfed the site fromsrc/generated/*.json, produced by Python scripts this branch removes. So the port had to change the data layer, not just move files:catalog.tsnow readsagents.lock.jsondirectly (gainingcommand,configPathandgroups, which the explorer and demo display), and a newrelease-channel.tsmaps a GitHub release into the channel shape the download and security pages already consume. Elevenas unknown as/as anycasts are gone with the untyped JSON imports they existed to paper over.catalog.tsalso imports its types fromexplorer.tsinstead of redeclaring them — the duplicate declarations were how a new protocol could reach the pages while the explorer still called it unsupported.A data race in the installer
go test -race -cover ./...failed oninternal/process, and it turned out not to be a test artifact.exec.Cmdcopies stdout and stderr on separate goroutines, so the listener passed toRunWithOutputwas entered from both at once. The install runtime hands it a closure that redacts and forwards (internal/app/install.go:373), holding no lock. That is a production data race, and it triggers whenever a command writes to both streams — npm does on every install.The lock now lives in the runner: "calls are serialised" is the contract a listener should be able to rely on, and both
streamWriters share one mutex (a mutex per writer would leave the two goroutines taking different locks and entering the listener together anyway).The existing tests only caught this by accident. They left stderr empty, so
-coveradding aGOCOVERDIR not setwarning to the child's stderr was what exposed the race at all — and that same warning polluted three assertions about captured output. The helper now gets its own scratchGOCOVERDIR, andTestOSRunnerSerialisesListenerAcrossStreamsdrives 50 interleaved writes per stream through an unlocked listener. Reverting the fix makes it fail withDATA RACE; I checked.A fresh clone could not build
manifest_embed.gosays a checked-in.keepmakes the stage-0 shell buildable before Vite runs. The file was never in the repository: twodist/rules excluded the directory itself, and excluding a directory stops git descending into it, so the!frontend/dist/.keepexception already sitting in the root.gitignorecould never take effect.go:embed all:frontend/disttherefore had nothing to embed, andgo vet ./...failed on a fresh clone until someone happened to build the frontend first. Verified by cloning this branch and runninggo vet ./...with no frontend build — it now passes, and build output stays ignored.Three judgement calls in the site copy
The security pages asserted something the data cannot support. They claimed a specific macOS arm64 build passed a cleanroom review; the release feed carries no build provenance. They now state what is checkable against the release page — channel, version, platform, digest — and say the provenance conclusions are recorded by the release process rather than asserted by the page.
release-channel.tsreportsnative_build: falseandcleanroom: "not-recorded"for the same reason: those are the honest readings of "the feed cannot tell us", and assertingtruewould put a verification badge on the site that nothing checked.A verification gate lost its subject.
validate-build.mjsused to re-hash every artifact indist/downloads/against the digest the release index claimed, so a page could not print a checksum the file did not have. With artifacts on GitHub Releases there is no local file to hash. Removed, with a comment recording the cost: a wrong digest from the release feed now reaches the page unchallenged. Restoring an equivalent check means fetching and hashing each asset during validation.npm run buildwas failing after reporting success — it ended innpm run validate, butvalidatehad been deleted frompackage.json. Restored.Gates
Two environment notes for whoever runs this next:
go.modrequires Go 1.26.5 andproxy.golang.orgis unreachable from here, so the automatic toolchain download fails. I installed 1.26.5 fromdl.google.com(checksum verified againstgo.dev/dl) and usedGOPROXY=https://goproxy.cnwithGOSUMDB=sum.golang.google.cn.go.sumis unchanged — no hash was substituted, and the existing entries were sufficient.GITHUB_TOKENis effectively required for the site build. Unauthenticated GitHub API access is 60 requests/hour and a 32-page build exceeds it; over the limit the build fails outright rather than degrading.The 23 skips are 8 pre-existing viewport skips plus 15 because this repository has no published releases —
getLatestRelease()returns null, so the download page renders its "not published yet" notice and the platform-picker tests have nothing to drive. They skip with a stated reason rather than being deleted, so they still catch a regression once artifacts exist. PR #2's own baseline behaves the same way; I checked before changing anything.🤖 Generated with Claude Code