Problem
A full-stack tic-tac-toe dogfood session (scaffolded with npm create webjs@latest, cli 0.10.32 / core 0.7.30 / server 0.8.43) reached a good final app in both dark and light themes, but the agent had to re-write code four times after writing the lit/React-shaped guess, booting it, seeing it fail in the browser, and diagnosing the webjs divergence. These are agent iteration loops in the precise sense (wrong-code, run, discover, rewrite), caused by webjs being new (no training data), not by unclear prompts.
Since the scaffold is the first-class training data for agents learning webjs, the durable fix is to make the correct idiom unmissable in what webjs create generates and in the READ-FIRST gotchas doc, plus fix the underlying framework behaviour where it is a genuine bug.
Every claim below was verified against the framework source before fixing (repros in the PR). Two of the four original claims were re-scoped after investigation; one earlier candidate (.env not gitignored) was disproved (the scaffold template has ignored .env since June, so the session diagnosis was wrong). This is deliberate: only genuine gaps are fixed.
Loop B (VERIFIED framework bug, re-scoped). A multi-hole attribute goes stale when only a later hole changes
Originally filed as "unkeyed list mis-patch". Investigation disproved that (reconcileArray is provably positional and correct for a fixed-length list). The real bug: a mixed attribute (class="base ${a} ${b}") is anchored at its first hole in render-client.js, with the later holes as noop parts. updateInstance's per-hole dirty-check then drops a change confined to a later hole, so the attribute keeps its stale value. Minimal repro: render class="s ${'A1'} ${'B1'}", then change only the second hole to 'B2'; the attribute stays "s A1 B1". This is a silent client-correctness bug affecting any element with two or more holes in one attribute. Fixed (point each non-anchor member at the anchor so any hole change rebuilds the whole attribute), with a red-before-fix regression test.
Loop A (VERIFIED, re-scoped). An interpolated <style> / <script> child is an SSR/client asymmetry
renderToString emits the interpolated content (verified), but the client parser deliberately drops a raw-text hole as noop (render-client.js L286-293: "per-render interpolation isn't supported"). So a component that renders html with a <style>${CSS}</style> paints correctly at SSR, then wipes it to empty on hydration. The client not supporting it is by design (the compile cache is keyed on strings); the gap is that it fails silently instead of being caught. Fix: a webjs check rule flagging an interpolated child of <style>/<script>, plus a gotchas entry pointing at static styles / layout CSS / css.
Loop E (VERIFIED, docs gap not a runtime change). A GET-action re-fetch returns the SSR seed
The SSR action seed (#472) is consume-once and keyed by hash + fn + args with no time bound, so the FIRST client call of a GET action consumes the SSR-time snapshot, even when that call happens after a mutation ("refresh the tally after a win" returned the pre-win value). The agent's rewrite to a deterministic optimistic bump is in fact the recommended webjs pattern, so this is a guidance gap, not a runtime bug: nothing warned that a GET-action re-read serves the seed. Fix: a gotchas entry ("a GET action's first client call resolves from the SSR seed; for a post-mutation refresh use optimistic update or revalidate(), not a re-call").
Loop H (VERIFIED scaffold gap). The generated theme apparatus fights a custom palette
The scaffold's generated app/layout.ts ships a dual .dark + [data-theme] token system and a head theme-detection script (packages/cli/lib/create.js ~L1136-1240). A custom single-theme :root palette was overridden by it (the board rendered white) until the agent stripped the whole apparatus. Complements #815/#816 (use a unique design): the wiring should not fight the design. Fix: mark the theme apparatus as one clearly-removable block in the generated layout with a one-line how-to.
Loops correctly AVOIDED this session (guidance already works, no action): the browser-test it()+chai trap (the agent checked ui: 'tdd' first, #837 item 2) and the page-action throw redirect() 307 footgun (returned an ActionResult, #837 item 1).
Scope
All four are fixed in ONE PR (no follow-up issues):
- B:
packages/core/src/render-client.js mixed-attribute commit, plus packages/core/test/rendering/ regression (red before fix) and Bun parity note.
- A: a new
webjs check rule in packages/server/src/check.js (rule table ~L58, follow no-redirect-in-api-route L138), plus agent-docs/lit-muscle-memory-gotchas.md.
- E:
agent-docs/lit-muscle-memory-gotchas.md + agent-docs/advanced.md (seed section) + a gallery note.
- H:
packages/cli/lib/create.js layout generator (removable-block comment); scaffold test asserts the marker.
- All four also land in
agent-docs/lit-muscle-memory-gotchas.md. Docs synced via webjs-doc-sync; scaffold synced + generate/boot/webjs check verified via webjs-scaffold-sync.
Acceptance criteria
Closes #845 is handled by the PR body.
Problem
A full-stack tic-tac-toe dogfood session (scaffolded with
npm create webjs@latest, cli 0.10.32 / core 0.7.30 / server 0.8.43) reached a good final app in both dark and light themes, but the agent had to re-write code four times after writing the lit/React-shaped guess, booting it, seeing it fail in the browser, and diagnosing the webjs divergence. These are agent iteration loops in the precise sense (wrong-code, run, discover, rewrite), caused by webjs being new (no training data), not by unclear prompts.Since the scaffold is the first-class training data for agents learning webjs, the durable fix is to make the correct idiom unmissable in what
webjs creategenerates and in the READ-FIRST gotchas doc, plus fix the underlying framework behaviour where it is a genuine bug.Every claim below was verified against the framework source before fixing (repros in the PR). Two of the four original claims were re-scoped after investigation; one earlier candidate (
.envnot gitignored) was disproved (the scaffold template has ignored.envsince June, so the session diagnosis was wrong). This is deliberate: only genuine gaps are fixed.Loop B (VERIFIED framework bug, re-scoped). A multi-hole attribute goes stale when only a later hole changes
Originally filed as "unkeyed list mis-patch". Investigation disproved that (
reconcileArrayis provably positional and correct for a fixed-length list). The real bug: a mixed attribute (class="base ${a} ${b}") is anchored at its first hole inrender-client.js, with the later holes asnoopparts.updateInstance's per-hole dirty-check then drops a change confined to a later hole, so the attribute keeps its stale value. Minimal repro: renderclass="s ${'A1'} ${'B1'}", then change only the second hole to'B2'; the attribute stays"s A1 B1". This is a silent client-correctness bug affecting any element with two or more holes in one attribute. Fixed (point each non-anchor member at the anchor so any hole change rebuilds the whole attribute), with a red-before-fix regression test.Loop A (VERIFIED, re-scoped). An interpolated
<style>/<script>child is an SSR/client asymmetryrenderToStringemits the interpolated content (verified), but the client parser deliberately drops a raw-text hole asnoop(render-client.jsL286-293: "per-render interpolation isn't supported"). So a component that rendershtmlwith a<style>${CSS}</style>paints correctly at SSR, then wipes it to empty on hydration. The client not supporting it is by design (the compile cache is keyed onstrings); the gap is that it fails silently instead of being caught. Fix: awebjs checkrule flagging an interpolated child of<style>/<script>, plus a gotchas entry pointing atstatic styles/ layout CSS /css.Loop E (VERIFIED, docs gap not a runtime change). A GET-action re-fetch returns the SSR seed
The SSR action seed (#472) is consume-once and keyed by hash + fn + args with no time bound, so the FIRST client call of a GET action consumes the SSR-time snapshot, even when that call happens after a mutation ("refresh the tally after a win" returned the pre-win value). The agent's rewrite to a deterministic optimistic bump is in fact the recommended webjs pattern, so this is a guidance gap, not a runtime bug: nothing warned that a GET-action re-read serves the seed. Fix: a gotchas entry ("a GET action's first client call resolves from the SSR seed; for a post-mutation refresh use optimistic update or
revalidate(), not a re-call").Loop H (VERIFIED scaffold gap). The generated theme apparatus fights a custom palette
The scaffold's generated
app/layout.tsships a dual.dark+[data-theme]token system and a head theme-detection script (packages/cli/lib/create.js~L1136-1240). A custom single-theme:rootpalette was overridden by it (the board rendered white) until the agent stripped the whole apparatus. Complements #815/#816 (use a unique design): the wiring should not fight the design. Fix: mark the theme apparatus as one clearly-removable block in the generated layout with a one-line how-to.Loops correctly AVOIDED this session (guidance already works, no action): the browser-test
it()+chai trap (the agent checkedui: 'tdd'first, #837 item 2) and the page-actionthrow redirect()307 footgun (returned anActionResult, #837 item 1).Scope
All four are fixed in ONE PR (no follow-up issues):
packages/core/src/render-client.jsmixed-attribute commit, pluspackages/core/test/rendering/regression (red before fix) and Bun parity note.webjs checkrule inpackages/server/src/check.js(rule table ~L58, followno-redirect-in-api-routeL138), plusagent-docs/lit-muscle-memory-gotchas.md.agent-docs/lit-muscle-memory-gotchas.md+agent-docs/advanced.md(seed section) + a gallery note.packages/cli/lib/create.jslayout generator (removable-block comment); scaffold test asserts the marker.agent-docs/lit-muscle-memory-gotchas.md. Docs synced viawebjs-doc-sync; scaffold synced + generate/boot/webjs checkverified viawebjs-scaffold-sync.Acceptance criteria
<style>/<script>child fails awebjs checkrule with an actionable message; counterfactual test red before the fix.app/layout.tsmarks the theme apparatus as one removable block; a scaffold test asserts the marker.agent-docs/lit-muscle-memory-gotchas.md.webjs creategenerate + boot +webjs checkverified; tests at every layer touched.Closes #845 is handled by the PR body.