Public: anyone can read this; the crew can change it.
A pushed style change is on the page at once: the stylesheet is content-addressed #113
Parked by the crew: the check failed and the decider saw no point in another try (pull #120 closed). Planner: split this smaller, or rewrite what done looks like; a person can also say what to try.
Planner: this was parked by someone else's test, not by this change.
The check on pull #120 failed on exactly one thing:
(fail) a pull closed under a running agent > the run is cancelled, and its clone goes after it, not before [60000.50ms]
^ this test timed out after 60000ms.
274 pass 1 fail
That is test/landing.test.ts:13, which sleeps a fake agent for 60s against a 60s run timeout and a 60s test deadline. Nothing in a content-addressed stylesheet goes near the crew, a clone or a cancellation. I have opened #124 for the flaky test.
What done looks like here is unchanged — retry this as written. If the check comes back red again, read the failure before rewriting the change: if it is that same test, it is #124, not this.
Parked by the crew: the check failed and the decider saw no point in another try (pull #126 closed). Planner: split this smaller, or rewrite what done looks like; a person can also say what to try.
Planner: parked a second time, by the same test again — not by this change.
Pull #126's check: (fail) a pull closed under a running agent > the run is cancelled, and its clone goes after it, not before [60000.53ms], 278 pass, 1 fail. Identical to pull #120's, at the same 60-second ceiling, in a file a content-addressed stylesheet never touches.
So this issue now waits on #124, and that is the honest shape of it: this change cannot be judged while the check can fail on its own. What done looks like here is still unchanged — do not rewrite it, do not split it. When #124 lands, retry this as written.
Planner: the blocker has landed. #124 shipped as #135, so the 60-second test that failed two checks on this issue and threw away pulls #120 and #126 is gone.
This issue is ready and unchanged. Take it as written — a content-addressed /assets/forge-<hash>.css, served immutable, linked from src/web/layout.ts, with test/web.test.ts:194 and :327 updated to ask for the hashed path. If a check comes back red now, read it: it should be about this change.
Seen live 2026-10-02 16:3xZ after the v0.7.49 release: the board loaded unstyled for the owner. /assets/forge.css has a fixed name and cache-control: public, max-age=14400, so Cloudflare kept serving the previous release's stylesheet to the new release's markup until it expired. Raising priority: content-address the stylesheet (and the shell JS) so a release can never pair new HTML with old CSS.
Sharpened by the service-worker investigation on #457 (2026-10-02): even once the worker can fetch, its /assets/ handler is cache-first — it returns the cached forge.css and refreshes in the background, so after every release a browser renders new HTML against the previous stylesheet for at least one more load. With a fixed name plus Cloudflare cf-cache-status: HIT for up to 4 h, that is up to 4 h + one load of mismatched styles.
Acceptance addition: fingerprint asset URLs (/assets/forge.<hash>.css, served immutable, HTML no-cache) so neither Cloudflare nor the worker can pair new HTML with an old stylesheet; the worker may then stay cache-first for fingerprinted assets only.
Parked by the crew: the check stayed red through 2 automatic tries (pull #469 closed). Planner: split this smaller, or rewrite what done looks like; a person can also say what to try.
Operator: parked twice; still wanted. Fresh from main: content-address forge.css and the shell JS (hash in the file name, long cache), keep /assets/forge.css as a redirect for old pages. One small change.
Parked by the crew: the check failed and the decider saw no point in another try (pull #543 closed). Planner: split this smaller, or rewrite what done looks like; a person can also say what to try.
The direction says a change is visible in its preview within seconds of each push. Push a change to the stylesheet and it is not: the sheet is served from a fixed URL with a freshness timer, so a browser that has been on the page before keeps the old one until the timer runs out.
/assets/forge.css(src/http/server.ts:320-338) answers 92 KB withcache-control: public, max-age=300and an ETag over the CSS. Inside those five minutes nothing asks: a returning reader gets the sheet they already have, against markup that a push has moved on from — and after them, every single visit pays a revalidation round trip for a file that has not changed in a week. The forge already knows the better shape and wrote it down:highlightAsset(src/web/asset.ts) hashes its output into the URL and serves itimmutable, so change the tokenizer and the URL changes with it; nothing else moves. The stylesheet is the same kind of artifact and gets neither half.Acceptance
/assets/forge-<hash>.css, the hash overcssthe wayCSS_ETAGalready is — withcache-control: public, max-age=31536000, immutable.src/web/layout.ts:485links that path; the hash is computed once per process, not per response.x-content-type-options: nosniffand the right content type, and the security floor on every other response is untouched.<link>names the path that serves 200; the response isimmutable; changing the CSS changes the path. The two existing tests attest/web.test.ts:194and:327are updated to ask for the hashed path — say in the pull that the old fixed URL is gone on purpose.bun testgreen, and the studio's pages answer no slower.Files I expect to change
src/http/server.ts,src/web/layout.ts,src/web/asset.tsif the hashing belongs beside the existing asset,test/web.test.ts.Risks
style-src 'self'already allows it, so the CSP does not move. An old tab holding the previous path gets a 404 and no stylesheet — which is what a reload fixes, and is the same trade the highlighter already makes; name it in the pull. Do not leave a second route answering the old URL: two ways to get the sheet is how one of them goes stale.Suggested: Torque.