You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
While scaffolding a simple full-stack todo app at ~/Documents/Projects/webjs-test (single-user, Node 24+, SQLite, full-stack template), the agent (myself) burned five write → check → rewrite cycles before webjs check went green and webjs typecheck exited 0. Every cycle was for a fix the agent already had enough context to write correctly the first time, given a sharper signal from the toolchain. This pattern has surfaced repeatedly (the host filed a prior issue for the same shape; #449 already exists for the server-import variant of this loop, and #789 / #789 closed the docs side of it). The remaining gap is the SUM of small per-cycle frictions, not any one of them.
Concretely, the cycles were:
Cycle 1 — transitive server-only import via a type-only module. Agent wrote modules/todos/types.ts that re-exported Todo from db/schema.server.ts, then app/page.ts registered the component AND called the server query, so the page elision verdict shipped the page to the browser with the schema in its closure. no-server-import-in-browser-module fired AFTER 100+ lines of code were written. Fix: move Todo to a JSDoc typedef in types.ts, drop the schema import, import the component in app/layout.ts (where the layout is the carrier) rather than app/page.ts. Took two rewrites because both halves (typedef split AND carrier choice) had to land.
Cycle 2 — Drizzle 1.0.0-rc.3 query API mismatch. Agent wrote the canonical db.select({ id, completed }).from(todos).where(eq(todos.id, id)) from agent-docs/recipes.md and got error TS2554: Expected 0 arguments, but got 1 (the typed overload does not match the docs example in rc.3). Same family on db.delete(todos).where(...).returning({ id }) (rc.3 has only the no-arg .returning() overload). Fix: switch to the relational-query API db.query.todos.findFirst({ where: { id } }) everywhere. Three rewrites.
Cycle 3 — WebComponent shadowing TS errors. Agent declared a class field private title = '' (overrides the inherited title on WebComponentBase, error TS2415 "private in type but not in base"); declared a method remove(t) (overrides HTMLElement.remove(), error TS2416). Rename + restart. Two rewrites.
Cycle 4 — framework Signal export shape. Agent imported type SignalState (does not exist) and type Signal (the export is a value, the class, not a type). Two TS rewrites before settling on let TS infer.
Cycle 5 — app/page.ts registration placement. Even after fixing the transitive import, no-server-import-in-browser-module still fired because the page BOTH registered the interactive component (so it shipped) AND transitively imported the schema. AGENTS.md has the rule ("register it in a layout.{ts,js} instead so the page elides again") but it is buried under carrier-and-elision prose. Agent missed it on the first read; the check was correct, the agent needed two attempts. One rewrite.
Total: ~10–15 minutes of throw-away edits, all because the agent either wrote the wrong shape the first time OR missed a one-paragraph rule buried in a 60 KB AGENTS.md. The PR is the same size either way; the iteration cost is pure overhead.
Design / approach
Three coordinated fixes, each addressing a cycle class:
A. Sharper webjs check messages — name the file chain, not the leaf.
The current no-server-import-in-browser-module message looks like:
This page ships to the browser (the build does not elide it) but transitively imports the server-only module db/schema.server.ts (app/page.ts -> … -> db/schema.server.ts).
The … -> elides the actual chain. An agent cannot tell from this whether the broken edge is in app/page.ts itself (carrier problem, cycle 5) or in a types module it forgot about (cycle 1). The message needs the full chain AND a one-line recommendation picked from the chain analysis. Concretely:
Print the full transitive chain (app/page.ts → modules/todos/components/todo-list.ts → modules/todos/types.ts → db/schema.server.ts → db/connection.server.ts), not ….
Detect and recommend the carrier fix when the chain's leaf is reached via a *.server.ts import from a *.ts types-shaped module, naming the immediate cycle class.
Detect and recommend the layout-registration fix when the carrier (the importer of *.server.ts) is a page.ts whose only browser signal is registering a component.
B. agent-docs/ updates that codify the right shape, with the next agent finding the canonical example in <30 seconds.
Three additions:
agent-docs/types-and-mutations.md (new): "Where do Todo, ActionResult<T>, etc. live when the consumer ships to the browser?" Codify: server-derived types stay in the action file's local JSDoc typedef OR in modules/<feature>/types.ts as a host-interface-style JSDoc (no runtime imports from *.server.ts or db/). Show the wrong + right chain in code.
agent-docs/components.md addendum: list the inherited members agent code cannot shadow without renaming (HTMLElement.remove, WebComponentBase.title, plus check for any other instance fields on the base). One-table summary, no scare copy.
agent-docs/orm.md (new OR fold into recipes.md): the rc.3 Drizzle query surface actually installed under drizzle-orm@^1.0.0-rc.3. Two patterns preferred: (1) db.query.<table>.findFirst({ where: { id } }) / .findMany({ orderBy: ... }) for simple reads, (2) db.insert/update/delete(t).set(...).where(eq(...)) for mutations, with NO .returning(fields) (use no-arg .returning() and pull fields locally). Mark the older db.select({...}).from(...) API as a gotcha tied to a specific Drizzle overload.
C. Pre-edit hook to catch cycle 1 + cycle 5 BEFORE the file lands.
.claude/hooks/check-server-imports.sh (a PreToolUse Edit hook, the same shape as the existing require-tests-with-src.sh): on every Edit / Write to a *.ts file outside *.server.ts, run a static walk: for each non-server .ts file the file imports, recursively check that none transitively imports db/*.server.ts / lib/*.server.ts. If a violation is found, BLOCK the edit and surface the chain. This is the same check rule webjs check runs in the post-edit pass, but moved to pre-edit. The cycle-5 carrier choice (page vs layout) is harder to catch pre-edit; document it in the new agent-docs/types-and-mutations.md and let the post-edit message handle it.
This is similar to .claude/hooks/require-tests-with-src.sh (added in an earlier round). The pattern is: same rule as webjs check, moved earlier in the loop, so the agent never writes the violating file.
Implementation notes (for the implementing agent)
A. Sharper check messages: edit packages/server/src/check.js. The rule lives under noServerImportInBrowserModule (grep for the rule id). Replace the … -> chain collapse with the full chain (already available in the rule's static walk — see the walk helper that follows imports), and branch on the leaf shape (types-only module vs page/layout carrier) to pick a recommendation string. Test in packages/server/test/check/server-import.test.js (or extend an existing file). Run with a fixture like the one in the Problem section; assert both the chain text and the right recommendation.
B. Docs: new files at agent-docs/types-and-mutations.md, agent-docs/orm.md; append to agent-docs/components.md. The CONTRIB protocol (per root AGENTS.md "Keep it in sync whenever behaviour changes") wants these in the same PR as the rule change. Cross-link from agent-docs/lit-muscle-memory-gotchas.md (the existing "shipped-to-the-browser means closure must close" entry is cycle 1 — fold that pointer here or expand it).
C. Hook: packages/cli/templates/.claude/hooks/check-server-imports.sh, registered in packages/cli/templates/.claude/settings.json under a PreToolUse:Edit matcher (PreToolUse:Write for new files). Implementation: a small node script that walks the import graph of the file being written, recursing through non-*.server.ts imports and printing the chain. Exit non-zero on a violation; exit 0 otherwise. The scaffold has a clear template pattern in packages/cli/templates/.claude/hooks/require-tests-with-src.sh to mirror. Note this template ships from the CLI; the repo copy lives alongside (linked by the scaffold).
Tests + docs: each fix needs a fixture. The check rule change in (A) needs a regression test (write the Cycle-1 chain in a temp app, run check, assert the message contains the full chain AND the right recommendation). The docs (B) need a counterfactual "before-and-after" example under agent-docs/types-and-mutations.md#counterfactual. The hook (C) needs an e2e: scaffold a temp app, write the violating chain, run the hook's filter, assert it blocks. All three under packages/server/test/check/ (rule) and packages/cli/test/scaffold-integrity/ (hook + docs presence).
Acceptance criteria
no-server-import-in-browser-module message prints the FULL transitive chain, no … -> truncation.
For the same message, when the chain's leaf is reached via a types-only *.{ts,js} module that re-exports from a server file, the recommendation names typedef relocation as the fix. When the chain's carrier is a page.ts that only ships because of a <my-el> registration, the recommendation names the layout-registration fix.
agent-docs/types-and-mutations.md exists with both shapes (wrong / right) and references the recipes.md todo example.
agent-docs/components.md lists the inherited fields/methods agent code cannot shadow without renaming (title, remove, plus any others on WebComponentBase).
agent-docs/orm.md codifies the rc.3 Drizzle patterns preferred for the scaffold's installed version (relational query for reads, no db.select({...}) overload, no .returning({...})).
A counterfactual proves each cycle-fix works: write the offending chain in a fixture, run the new check, expect green on the FIRST attempt (no cycles).
packages/cli/templates/.claude/hooks/check-server-imports.sh exists, registered in templates/.claude/settings.json, runs as a PreToolUse:Edit, blocks the same chain at write time with a non-zero exit.
A new test under packages/server/test/check/server-import-message.test.js (or extension to the existing file) pins the message shape.
A new test under packages/cli/test/scaffold-integrity/hook-blocks-server-import.test.js pins that the hook fires on a real scaffolded-app edit.
Docs site (docs/app/docs/troubleshooting) cross-linked from the new agent-docs/ files for the "I imported the schema into types.ts and now check fires" symptom.
Counterfactual
If reverted, an agent rebuilding the todo-app reproduces cycles 1, 3, 5 with the first webjs check run, taking ≥ 15 minutes of edits before green. With all three landed, the first-pass scaffolded todo app passes webjs check on the FIRST render — confirmed by running the fixture under packages/cli/test/scaffold-integrity/first-pass-check.test.js.
Problem
While scaffolding a simple full-stack todo app at
~/Documents/Projects/webjs-test(single-user, Node 24+, SQLite, full-stack template), the agent (myself) burned five write → check → rewrite cycles beforewebjs checkwent green andwebjs typecheckexited 0. Every cycle was for a fix the agent already had enough context to write correctly the first time, given a sharper signal from the toolchain. This pattern has surfaced repeatedly (the host filed a prior issue for the same shape; #449 already exists for the server-import variant of this loop, and #789 / #789 closed the docs side of it). The remaining gap is the SUM of small per-cycle frictions, not any one of them.Concretely, the cycles were:
Cycle 1 — transitive server-only import via a type-only module. Agent wrote
modules/todos/types.tsthat re-exportedTodofromdb/schema.server.ts, thenapp/page.tsregistered the component AND called the server query, so the page elision verdict shipped the page to the browser with the schema in its closure.no-server-import-in-browser-modulefired AFTER 100+ lines of code were written. Fix: moveTodoto a JSDoc typedef intypes.ts, drop the schema import, import the component inapp/layout.ts(where the layout is the carrier) rather thanapp/page.ts. Took two rewrites because both halves (typedef split AND carrier choice) had to land.Cycle 2 — Drizzle 1.0.0-rc.3 query API mismatch. Agent wrote the canonical
db.select({ id, completed }).from(todos).where(eq(todos.id, id))fromagent-docs/recipes.mdand goterror TS2554: Expected 0 arguments, but got 1(the typed overload does not match the docs example in rc.3). Same family ondb.delete(todos).where(...).returning({ id })(rc.3 has only the no-arg.returning()overload). Fix: switch to the relational-query APIdb.query.todos.findFirst({ where: { id } })everywhere. Three rewrites.Cycle 3 —
WebComponentshadowing TS errors. Agent declared a class fieldprivate title = ''(overrides the inheritedtitleonWebComponentBase, error TS2415 "private in type but not in base"); declared a methodremove(t)(overridesHTMLElement.remove(), error TS2416). Rename + restart. Two rewrites.Cycle 4 — framework
Signalexport shape. Agent importedtype SignalState(does not exist) andtype Signal(the export is a value, the class, not a type). Two TS rewrites before settling onlet TS infer.Cycle 5 —
app/page.tsregistration placement. Even after fixing the transitive import,no-server-import-in-browser-modulestill fired because the page BOTH registered the interactive component (so it shipped) AND transitively imported the schema. AGENTS.md has the rule ("register it in a layout.{ts,js} instead so the page elides again") but it is buried under carrier-and-elision prose. Agent missed it on the first read; the check was correct, the agent needed two attempts. One rewrite.Total: ~10–15 minutes of throw-away edits, all because the agent either wrote the wrong shape the first time OR missed a one-paragraph rule buried in a 60 KB
AGENTS.md. The PR is the same size either way; the iteration cost is pure overhead.Design / approach
Three coordinated fixes, each addressing a cycle class:
A. Sharper
webjs checkmessages — name the file chain, not the leaf.The current
no-server-import-in-browser-modulemessage looks like:The
… ->elides the actual chain. An agent cannot tell from this whether the broken edge is inapp/page.tsitself (carrier problem, cycle 5) or in a types module it forgot about (cycle 1). The message needs the full chain AND a one-line recommendation picked from the chain analysis. Concretely:app/page.ts → modules/todos/components/todo-list.ts → modules/todos/types.ts → db/schema.server.ts → db/connection.server.ts), not….*.server.tsimport from a*.tstypes-shaped module, naming the immediate cycle class.*.server.ts) is apage.tswhose only browser signal is registering a component.B.
agent-docs/updates that codify the right shape, with the next agent finding the canonical example in <30 seconds.Three additions:
agent-docs/types-and-mutations.md(new): "Where doTodo,ActionResult<T>, etc. live when the consumer ships to the browser?" Codify: server-derived types stay in the action file's local JSDoc typedef OR inmodules/<feature>/types.tsas a host-interface-style JSDoc (no runtime imports from*.server.tsordb/). Show the wrong + right chain in code.agent-docs/components.mdaddendum: list the inherited members agent code cannot shadow without renaming (HTMLElement.remove,WebComponentBase.title, plus check for any other instance fields on the base). One-table summary, no scare copy.agent-docs/orm.md(new OR fold into recipes.md): the rc.3 Drizzle query surface actually installed underdrizzle-orm@^1.0.0-rc.3. Two patterns preferred: (1)db.query.<table>.findFirst({ where: { id } })/.findMany({ orderBy: ... })for simple reads, (2)db.insert/update/delete(t).set(...).where(eq(...))for mutations, with NO.returning(fields)(use no-arg.returning()and pull fields locally). Mark the olderdb.select({...}).from(...)API as a gotcha tied to a specific Drizzle overload.C. Pre-edit hook to catch cycle 1 + cycle 5 BEFORE the file lands.
.claude/hooks/check-server-imports.sh(aPreToolUseEdit hook, the same shape as the existingrequire-tests-with-src.sh): on everyEdit/Writeto a*.tsfile outside*.server.ts, run a static walk: for each non-server.tsfile the file imports, recursively check that none transitively importsdb/*.server.ts/lib/*.server.ts. If a violation is found, BLOCK the edit and surface the chain. This is the same check rulewebjs checkruns in the post-edit pass, but moved to pre-edit. The cycle-5 carrier choice (page vs layout) is harder to catch pre-edit; document it in the newagent-docs/types-and-mutations.mdand let the post-edit message handle it.This is similar to
.claude/hooks/require-tests-with-src.sh(added in an earlier round). The pattern is: same rule aswebjs check, moved earlier in the loop, so the agent never writes the violating file.Implementation notes (for the implementing agent)
packages/server/src/check.js. The rule lives undernoServerImportInBrowserModule(grep for the rule id). Replace the… ->chain collapse with the full chain (already available in the rule's static walk — see thewalkhelper that follows imports), and branch on the leaf shape (types-only module vs page/layout carrier) to pick a recommendation string. Test inpackages/server/test/check/server-import.test.js(or extend an existing file). Run with a fixture like the one in the Problem section; assert both the chain text and the right recommendation.agent-docs/types-and-mutations.md,agent-docs/orm.md; append toagent-docs/components.md. The CONTRIB protocol (per rootAGENTS.md"Keep it in sync whenever behaviour changes") wants these in the same PR as the rule change. Cross-link fromagent-docs/lit-muscle-memory-gotchas.md(the existing "shipped-to-the-browser means closure must close" entry is cycle 1 — fold that pointer here or expand it).packages/cli/templates/.claude/hooks/check-server-imports.sh, registered inpackages/cli/templates/.claude/settings.jsonunder aPreToolUse:Editmatcher (PreToolUse:Writefor new files). Implementation: a smallnodescript that walks the import graph of the file being written, recursing through non-*.server.tsimports and printing the chain. Exit non-zero on a violation; exit 0 otherwise. The scaffold has a clear template pattern inpackages/cli/templates/.claude/hooks/require-tests-with-src.shto mirror. Note this template ships from the CLI; the repo copy lives alongside (linked by the scaffold).agent-docs/types-and-mutations.md#counterfactual. The hook (C) needs an e2e: scaffold a temp app, write the violating chain, run the hook's filter, assert it blocks. All three underpackages/server/test/check/(rule) andpackages/cli/test/scaffold-integrity/(hook + docs presence).Acceptance criteria
no-server-import-in-browser-modulemessage prints the FULL transitive chain, no… ->truncation.*.{ts,js}module that re-exports from a server file, the recommendation names typedef relocation as the fix. When the chain's carrier is apage.tsthat only ships because of a<my-el>registration, the recommendation names the layout-registration fix.agent-docs/types-and-mutations.mdexists with both shapes (wrong / right) and references the recipes.md todo example.agent-docs/components.mdlists the inherited fields/methods agent code cannot shadow without renaming (title,remove, plus any others onWebComponentBase).agent-docs/orm.mdcodifies the rc.3 Drizzle patterns preferred for the scaffold's installed version (relational query for reads, nodb.select({...})overload, no.returning({...})).packages/cli/templates/.claude/hooks/check-server-imports.shexists, registered intemplates/.claude/settings.json, runs as aPreToolUse:Edit, blocks the same chain at write time with a non-zero exit.packages/server/test/check/server-import-message.test.js(or extension to the existing file) pins the message shape.packages/cli/test/scaffold-integrity/hook-blocks-server-import.test.jspins that the hook fires on a real scaffolded-app edit.docs/app/docs/troubleshooting) cross-linked from the newagent-docs/files for the "I imported the schema into types.ts and now check fires" symptom.Counterfactual
If reverted, an agent rebuilding the todo-app reproduces cycles 1, 3, 5 with the first
webjs checkrun, taking ≥ 15 minutes of edits before green. With all three landed, the first-pass scaffolded todo app passeswebjs checkon the FIRST render — confirmed by running the fixture underpackages/cli/test/scaffold-integrity/first-pass-check.test.js.