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
ui:add describes itself as scaffolding "the Fresh page + island + query-loader triad for a data
screen", and the generated app AGENTS.md tells every coding agent to reach for it first. What it
emits is a page with a () => ({}) layer loader, an island that is a useSignal(0) counter button,
and a file containing export const queryLoaders = {} as const;. The one command the framework
points agents at produces a counter where a data screen was promised — which is the measurable
mechanism behind agents hand-rolling their own screens.
Evidence
research/repo-audit/web-layer.md §8 (full emission breakdown) and gap register item 9; research/repo-audit/mcp-cli.md §3.5.
Repo, verified at fac9e339042c, packages/cli/src/kernel/application/ui/web-scaffold.ts:
:15-42 — scaffoldUiPage writes exactly three files; the page's only layer is .withLayer('<name>', () => <Island/>, () => ({})) with no loader, no withResource, no query.
:34 + :66-68 — the island content is signalIslandTemplate, i.e. const count = useSignal(0); return <button …>{count}</button>;.
:37 — the third file is literally export const queryLoaders = {} as const;.
:51 — ui:add island --query emits <QueryIsland><div>Name</div></QueryIsland>: a provider
around a static div, no useQuery, no key, no factory.
:60 — any pre-existing target file is a hard refuse; there is no --force and no --dry-run,
unlike every other generator (install-plugin-command.ts has both).
:20 — the route id is a hand-derived dotted string with no link to the generated manifest, and
the page declares createRouteReference inline instead of registering in router.ts.
Advertised behavior it contradicts: packages/cli/src/public/features/ui/add/add-ui-command.ts:26-28 (the description quoted above)
and packages/cli/src/kernel/templates/app/agent-conventions.ts:137-139 (the three commands
agents are told to use; line 137 promises "a typed Fresh route, a colocated hydrating island, and
its query-loader seam").
Three files, none of which loads data. Two island conventions from one command: ui:add page --island writes to routes/<seg>/(_islands)/, ui:add island writes to top-level islands/. UiAddCommandInput (add-ui-input.ts) omits route, island and query, so the exported public
type under-describes the public CLI.
Target contract
ui:add page <path> --island emits a working data screen — the minimal composable core of the #1354 slice, not a second generator:
The page uses the typed route surface (registered through router.ts/appRoutes, not an inline createRouteReference) and a real layer loader.
The (_shared) file contains a cache-first loader bound to a real query factory and returning cachedAt; it is not an empty object.
ui:add island --query emits an island that actually issues a query.
When the target app has no service/contract to bind, the command says so and names the verb to
run first rather than emitting a placeholder that type-checks and does nothing.
Island placement is one documented convention, with the divergence between the two commands
resolved explicitly.
--force and --dry-run exist, matching the rest of the CLI.
UiAddCommandInput declares every accepted option.
Acceptance
ui:add page <path> --island emits a page, loader and island that fetch and render real
contract-derived data in a scaffolded project.
The emitted island contains no useSignal counter and the emitted (_shared) file contains
no empty queryLoaders object.
ui:add island <Name> --query emits an island that issues a query with a factory-derived key.
The emitted page registers its route through router.ts/appRoutes.
Island placement follows one documented convention across both commands.
--force and --dry-run are supported and documented.
Running the command in an app with no bindable contract exits non-zero with the prerequisite
verb named, and writes no files.
Negative test: a golden test fails if the emitted island imports @preact/signals' useSignal
as its only behavior, or if the emitted loader module exports an empty object.
Negative test: the command's own --help text is asserted against the emitted file set, so
the description and the emission cannot diverge again.
gate: deno task e2e:cli run scaffold.runtime --cleanup scaffolds a page via this command and
type-checks the app containing it.
The proof is a generated screen, not a paragraph: scaffold.runtime runs the command and
type-checks the result, the app AGENTS.md example matches the real emission, and the Web Layer
how-to that names the triad shows the actual generated files. #1090 separately observes whether an
unfamiliar agent reaches for the command — that measurement stays on #1090.
Provenance
Seed run plan-fable5-remediation-roadmap--seed, PR #1347, 2026-08-08. Drafted from research/repo-audit/web-layer.md §8 and research/repo-audit/mcp-cli.md §3.5; every line
re-verified against worktree fac9e339042c. No GitHub mutation performed.
Filed from planning seed PR #1347 · source Draft-ID T2-04 · live issue #1357.
Summary
ui:adddescribes itself as scaffolding "the Fresh page + island + query-loader triad for a datascreen", and the generated app
AGENTS.mdtells every coding agent to reach for it first. What itemits is a page with a
() => ({})layer loader, an island that is auseSignal(0)counter button,and a file containing
export const queryLoaders = {} as const;. The one command the frameworkpoints agents at produces a counter where a data screen was promised — which is the measurable
mechanism behind agents hand-rolling their own screens.
Evidence
research/repo-audit/web-layer.md§8 (full emission breakdown) and gap register item 9;research/repo-audit/mcp-cli.md§3.5.fac9e339042c,packages/cli/src/kernel/application/ui/web-scaffold.ts::15-42—scaffoldUiPagewrites exactly three files; the page's only layer is.withLayer('<name>', () => <Island/>, () => ({}))with no loader, nowithResource, no query.:34+:66-68— the island content issignalIslandTemplate, i.e.const count = useSignal(0); return <button …>{count}</button>;.:37— the third file is literallyexport const queryLoaders = {} as const;.:51—ui:add island --queryemits<QueryIsland><div>Name</div></QueryIsland>: a provideraround a static div, no
useQuery, no key, no factory.:60— any pre-existing target file is a hard refuse; there is no--forceand no--dry-run,unlike every other generator (
install-plugin-command.tshas both).:20— the route id is a hand-derived dotted string with no link to the generated manifest, andthe page declares
createRouteReferenceinline instead of registering inrouter.ts.packages/cli/src/public/features/ui/add/add-ui-command.ts:26-28(the description quoted above)and
packages/cli/src/kernel/templates/app/agent-conventions.ts:137-139(the three commandsagents are told to use; line 137 promises "a typed Fresh route, a colocated hydrating island, and
its query-loader seam").
rickylabs/loombuilt routeswith hand-rolled tables and a 676-line island); verify(wave-five): does the shipped agent surface actually change agent behaviour? #1090 holds the observational box "an agent
building a data screen runs
ui:addor records why not".Current surface
Three files, none of which loads data. Two island conventions from one command:
ui:add page --islandwrites toroutes/<seg>/(_islands)/,ui:add islandwrites to top-levelislands/.UiAddCommandInput(add-ui-input.ts) omitsroute,islandandquery, so the exported publictype under-describes the public CLI.
Target contract
ui:add page <path> --islandemits a working data screen — the minimal composable core of the#1354 slice, not a second generator:
router.ts/appRoutes, not an inlinecreateRouteReference) and a real layer loader.(_shared)file contains a cache-first loader bound to a real query factory and returningcachedAt; it is not an empty object.QueryIsland+useIslandQuerybound to the factory'sclientKey, seeded withthe loader's data and
initialDataUpdatedAt(fix(scaffold): the canonical island never passes initialDataUpdatedAt, so the loader's cachedAt is computed, displayed and discarded #1360) — not a counter.ui:add island --queryemits an island that actually issues a query.run first rather than emitting a placeholder that type-checks and does nothing.
resolved explicitly.
--forceand--dry-runexist, matching the rest of the CLI.UiAddCommandInputdeclares every accepted option.Acceptance
ui:add page <path> --islandemits a page, loader and island that fetch and render realcontract-derived data in a scaffolded project.
useSignalcounter and the emitted(_shared)file containsno empty
queryLoadersobject.ui:add island <Name> --queryemits an island that issues a query with a factory-derived key.router.ts/appRoutes.--forceand--dry-runare supported and documented.UiAddCommandInputdeclaresroute,island,query(andapp, per fix(cli): every ui:* command writes to the workspace root instead of apps/<app>, and the E2E gate asserts the wrong root #1356).verb named, and writes no files.
@preact/signals'useSignalas its only behavior, or if the emitted loader module exports an empty object.
--helptext is asserted against the emitted file set, sothe description and the emission cannot diverge again.
deno task e2e:cli run scaffold.runtime --cleanupscaffolds a page via this command andtype-checks the app containing it.
Boundaries
--form/--partial/--stream, route-local(_components), ownership comments). If the owner consolidates, this issue is that generator'sminimal mode — state the decision, do not ship two generators.
example routes.
agent-run box to this issue's acceptance.
Docs/consumer proof
The proof is a generated screen, not a paragraph:
scaffold.runtimeruns the command andtype-checks the result, the app
AGENTS.mdexample matches the real emission, and the Web Layerhow-to that names the triad shows the actual generated files. #1090 separately observes whether an
unfamiliar agent reaches for the command — that measurement stays on #1090.
Provenance
Seed run
plan-fable5-remediation-roadmap--seed, PR #1347, 2026-08-08. Drafted fromresearch/repo-audit/web-layer.md§8 andresearch/repo-audit/mcp-cli.md§3.5; every linere-verified against worktree
fac9e339042c. No GitHub mutation performed.Filed from planning seed PR #1347 · source Draft-ID T2-04 · live issue #1357.