Repository navigation
Plan screens up front, deliver finished sites, keep multi-colour icons as images - #97
Merged
owjs3901 merged 25 commits intoOct 17, 2026
Conversation
An icon is emitted as a Box with maskImage + bg only when every paint in
it is one flat colour. The sameColor analysis let two kinds of paint slip
through:
- A visible non-SOLID paint (gradient, image, unreadable solid) counted
as Null, "no colour", so {gradient vector, solid, solid} merged to the
solid colour and became a mask. A mask keeps only the shape, so the
gradient face and the outline flattened into one purple blob (prime
/seo-geo "기대 효과" icons). Such a paint now counts as False.
- mergeSameColor skipped a Null before the first colour but spoiled the
colour with a Null after it, so layer order decided the answer. An
unpainted subtree is now neutral wherever it falls.
The fixture-compat viewport renderer masked whenever all variants were
assets and took the first grandchild's first fill as the colour; it now
masks only when every visible paint under the variants is the same SOLID.
No existing expectation changed: none of the fixtures encoded the bug.
New regression tests cover gradient + solid, gradient stroke, gradient
on top, image paint, two solids, all-same solid, and an unpainted vector.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ur SVG The generator now keeps multi-colour icons as <Image>, but hand-written code (or a hand "fix" that turns an <Image> icon into a mask) can still point maskImage at one, and nothing in the TSX shows it: the colours are in the file. With projectRoot, every root-relative url(...svg) inside a maskImage value (JSX string, JSX expression or css object) is resolved under the project's public directories (<root>/public first, then package public dirs) and the SVG is read with quick-xml. Fills and strokes are collected with inheritance and the black default, skipping <clipPath>/<mask>/<defs>/gradient bodies and zero-opacity or hidden paints, so a clipPath's fill="white" rect does not count. A url() paint (gradient/pattern), an <image>, or more than one distinct colour is a `multicolor-mask` warning naming the file and telling the caller to use <Image src>. Strict mode fails on it like any warning; verificationScope lists the rule when projectRoot is given. Path segments that could leave the public dir are refused, files over 2 MiB are not read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing files
Polling {jobId, jobAction:"status"} returned the whole job each time:
30-100 KB with every asset's exportDiagnostics/renderInfo, twice, because
assetJob repeated exportJob's body. responseFields on a poll were ignored.
- Job replies carry compact per-asset rows (assetId, status, byteLength,
sha256, errorCode/errorDetail, acquisitionState, outputPath,
requestedOutputPath, fileState, fileStateReason, suggestedFilename).
exportJob.assetDetail says how to read full rows; a completed job's
assetManifest still has them.
- responseFields/path on a poll select from the full reply (same grammar
and 64 KiB ceiling as an export selection, via a shared `walk`) and
return only the selections plus the job's progress.
- assetJob is now {aliasOf:"exportJob", jobId, state} instead of a copy.
The batch-merge CLI reads exportJob.assets and falls back to
assetJob.assets for batches saved before this.
- A requested file that is still `pending` carries fileStateReason: an
exported asset is held in memory until the projection-and-write stage.
Test expectation changes: tests that read assetJob.assets/calls/
assetTimeoutMs/assetProgress now read exportJob (the alias no longer
holds the body), and asset_jobs unit tests that inspect renderInfo or
quality ask for full rows. No assertion about values changed.
Tool description and usage guide: say that polls are compact and how to
select full rows, and that generated code masks only single-colour icons,
multi-colour icons stay <Image>, and an agent must not turn an <Image>
icon into a mask or recolour it by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ingest now returns screenTable: one row per plan screen and viewport with pass, fail, inconclusive or not-run and the reasons. A failed no-horizontal-overflow check reads as horizontal overflow, a navigation-failed error is pinned on the screen whose route it names, an unattributed page error leaves that width inconclusive, and a missing run or screen item is not-run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
devup_screen_plan maps every Figma screen to a URL before any page is written (action draft | confirm | status). draft groups frames drawn at several widths, proposes a route only from an existing App Router page, a nav label bound to an existing href, or a navigationTargets entry, and turns every open decision into one questions array shaped for the runtime's question tool, written to .devup/screen-plan.json and .md. confirm refuses a partial set of answers by id; status reports each URL's implementation and per-width result from a runtime audit, never counting a missing width as pass. devup_figma_export with projectRoot now answers plan_required instead of writing a page (pageScaffold write, outputPaths under app/, several frames with outputPaths) when no confirmed plan covers the frames, takes the pageScaffold route from the plan, and refuses a different one. Explore's body moved into a helper so draft lists screens through the same read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The own skill carries the workflow: list screens, draft, ask every question in one question-tool call, confirm, implement URL by URL, run the runtime audit, read status, and finish with the URL x width table. It is installable through devup_skills; the README describes the new tool and the export gate, and a changepack log marks devup-mcp as a minor change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er confirm draft now returns questionRounds next to the flat list: at most four questions of at most four options per round, every question exactly once, and a question with `when` always after the answer it depends on. confirm requires exactly the questions that apply and returns the missing ones as rounds. After confirm nothing is asked: the new assume action records what implementation decided on its own, and status and the plan's Markdown show every assumption; a frame outside the confirmed plan points at assume instead of a new draft. The invented <route>/<width> scheme is gone. Frames sharing a name are grouped across sections and asked about per group, recommending one responsive page for several widths in one section (or device-named sections) and separate pages across sections; separate pages bring a route question per frame in the same up-front batch, and a custom answer may list one route per frame. Layer-name guesses are never silent: a route matched by name is routeEvidence name-match and goes into one "Accept all / Review individually" question with per-item reviews behind it, and a modal detected by name says so. A data-figma-node-id already in a route's files is new evidence, and an optional .devup/screen-plan.config.json (modalKeywords, routeAliases, widthGrouping) counts as evidence and replaces the questions it answers. Explore fills each frame's section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…README The devup-implementation-plan skill drops "split only as far as the tool forces" and says to ask every round back to back before implementing, never during it, and to record assumptions instead; it documents the new group, split-route, heuristics and review questions and the optional .devup/screen-plan.config.json. Guide rule 2c, the export description pointer, the manifest entry, the README and the changepack note match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…design fixes Scope: only frames under the linked node are candidates; anything whose ancestry does not reach it is listed as outOfScope and never planned. Frames in trash, archive, old, deprecated, backup, draft, test, sandbox, playground or copy areas, and hidden or zero-opacity frames, are excluded with their reason, shown in the plan and its Markdown, and confirmed in one "accept the exclusions / review" question; excludeKeywords and includeKeywords in the project config change the words. A link that itself points into such an area is asked about once instead of excluded. Cross-section: the same name in different sections - device-named ones included - no longer gets a silent recommendation. A sections: question states the section names, frames, widths and node ids and why they look related, recommends separate pages unless every section names a device (saying so), and offers "The Figma structure is wrong: I will fix it". Duplicates in one section, one screen split across sections and a frame outside any section get structure: questions with the same option. A fix answer leaves those frames out of the route table and lists them under "Design fixes requested" with what to move or rename; status and the plan file report them, and the export gate refuses them and excluded frames whatever assumption is recorded. Rounds: maxQuestionsPerRound (default 4) sets the split, and askInstruction says a question tool with no cap takes the flat list in one call, where each conditional question now says in its text when it applies. The assumptions list in status' markdownTable no longer carries stray newlines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…EADME The devup-implementation-plan skill gains the hard rules: stay inside the linked scope and out of trash, archive and draft areas, never resolve a cross-section ambiguity yourself, and report frames marked for a design fix instead of building them. It describes the new questions, the flat list for uncapped question tools, and excludeKeywords/includeKeywords next to the other config entries. Guide rule 2c, the tool and export descriptions, the README, the manifest and the changepack note match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New devup-ui site checks, run per file in bundle mode: an interactive element (as="a"/"button", Link, button, onClick) with neither _hover nor _focusVisible is an interactive-states warning, a _hover with no transition is hover-transition info, and an App Router page or layout exporting neither metadata nor generateMetadata is a page-metadata warning. interaction_coverage counts elements with and without states for the screen plan's status. The one-component-per-file fixture page now exports metadata so its strict verdict stays about that rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Site completeness: draft records the defaults the agent fills in without asking - interaction states from Figma variants or existing tokens only, one shared transition, reduced motion, root and per-page metadata with openGraph, lang, alt text, image sizes, a not-found page - and asks, in the same up-front rounds, only for what needs a file or a fact: favicon and share image (offered from existing files and Figma logo/og nodes in scope, never generated), site name and URL when the project does not say, analytics when it shows signs, and motion beyond hover. The project's layout, .env and package.json, and .devup/screen-plan.config.json (favicon, ogImage, siteName, siteUrl, motion), pre-answer them. status reports favicon, metadata per URL, share image, hover/focus coverage, reduced motion, site name and URL as filled, asked-and-answered or missing, and lists the defaults. devup_ui_validate with projectRoot also checks single-file code for states and warns when the app has no icon. /devup: a new own skill, devup, carries the whole workflow; Claude Code offers it as /devup, and devup_skills install writes an OpenCode command at .opencode/command/devup.md beside it. Other runtimes are reported as unsupported rather than given an unverified path. devup_skills status and the skillGap hint report the command. Rule 2c and the export description now start the workflow on any request to build pages or a site from Figma, /devup or not; the instructions dropped the displayVersion sentence to stay under their byte cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Combine the two pieces of work for one pull request. Conflicts were the two long tool descriptions in server/mod.rs, both resolved by keeping every sentence from each side: - devup_figma_export: the compact poll text and the single-colour mask rule from multicolor-icon-mask, then the screen-plan pointer from this branch after the SECTION two-stage sentence. - devup_ui_validate: the multicolor-mask warning and the site completeness rules. guide.rs merged on its own (the mask rule and the compact poll note are in the guide body; INSTRUCTIONS is unchanged at 1456 bytes). The one changepack log now covers both pieces of work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The site checks now sort interactive elements into roles - button, icon button, link, nav item (in a nav or with aria-current), card (a link around an image), input, disclosure, click target - and judge each against what its role owes. New findings: focus-style (warning) for a hover with no focus style, too-much-motion (warning) for durations over 400ms, scale above 1.05 or an infinite animation, icon-button-label (warning) for a text-less button without aria-label, and button-active, motion-reduce and nav-current (info). role_coverage counts complete elements per role for the screen plan's status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…checklist
- defaults: an interaction recipe per element role (primary and secondary
buttons, text links, linked cards, nav items, icon buttons, inputs,
disclosures, mobile menus) with hard restraint rules, replacing the flat
hover/focus/active default; status counts each role against its recipe.
- View transitions, decided from the project's Next.js and React versions
(package-lock.json first) against the current Next.js guide: Next 16 with
React 19.3 uses import { ViewTransition } from 'react' and no flag, Next
15.2+ the experimental flag (recorded as an assumption), no Next the CSS
@view-transition rule, anything else skipped with its reason. A project
that already animates pages with its own library is asked instead.
status reports "view transitions: enabled (mechanism) / skipped (reason)".
- extraQuestions: the agent's own questions in the same shape go into the
up-front rounds; confirm returns them as extraAnswers.
- The devup-implementation-plan and devup skills and the OpenCode /devup
command end with a submission checklist (every URL, build, lint and
typecheck, validate, audit clean at every width, completeness, final
table); the guide gains rule 14 pointing at the recipe table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate step - framework.rs: an app runs on vinext when it has a vinext dependency, vinext scripts or a vite.config importing vinext - no next dependency needed - and on next when it has a next dependency; bun workspaces (apps/*) are followed, so projectRoot may be the repository or one app. devup_project_context ui and routes report `frameworks` with the evidence per app. - The screen plan builds the target app (asked when a repository holds several), reads its own package.json before the root's, and bun.lock before package-lock.json. - View transitions: vinext 1.0+ with React 19.3+ (react-dom and react-server-dom-webpack count) is enabled with React's ViewTransition, following the Next.js guide, vinext's compatibility suite and its own playground; status decides again from the installed versions, after the update. The next-CLI tree stays as the fallback. The doc URLs are in the code and in the plan's default. - Stack defaults: a new project starts from dev-five-git/service-template (asking only its app name and parts), a next-CLI app gets one migrate-to-vinext question, and the dependency step (bunx @dependency-check-updates/cli -d -u --rm) runs before implementing. assume records its outcome (dependencyUpdate, pins also as assumptions) and status shows a dependencies line. Config keys framework and updateDependencies pre-answer them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- devup-implementation-plan, devup and the OpenCode /devup command gain a "latest first" step before implementing: the dependency update at the repository root (with what -d, -u and --rm actually do, per the tool's --help, including that --rm also removes Cargo.lock and target/), then bun install, build, lint and typecheck, fixing code to new APIs and pinning only with a recorded reason, and current docs before any library API. The submission checklist uses the project's real scripts and notes that bun run test runs cargo tarpaulin, which fails on Windows. - devfive-frontend follows service-template's AGENTS.md: Next App Router run by vinext by default, service-template as the starting point, TS 7, oxlint via eslint-plugin-devup/oxlint-config, the React Compiler as a vinext option, and the real verification commands. - Guide rule 15 and the README describe the stack; INSTRUCTIONS unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A view-transition check follows its subject link to another route and passes only when a view transition ran: document.startViewTransition called (what React's <ViewTransition> does) or a cross-document pagereveal carrying one (the CSS @view-transition rule). An init script counts both in sessionStorage so the count survives a document change. The check navigates away, so the executor runs it after the screen's other checks and its screenshot. evaluateViewTransition judges the observation and is unit-tested; a browser test covers a same-document transition, a cross-document one and a plain link that must fail. The plan schema accepts the kind with a subject, and ingest names it in screen reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- View transitions are always on (react-view-transition): the dependency step brings every project to the latest Next and React, so the version-keyed trees for vinext, new projects and the next CLI are gone, with their tests. Skipped only for viewTransitions: false in the project config, or updateDependencies: false on a React older than 19.3. The decision names the runtime-audit view-transition check that proves it; experimental.viewTransition is added only if that check fails without it. - In a repository with several apps the target app is chosen from the apps' names and routes and the frames' own names (public site vs admin console): all-public frames go to the public app and all-admin frames to the admin app without a question, a mixed file maps each screen and asks only about screens of unclear side, and only frames that say nothing are asked once, recommending the public app. The evidence is recorded in defaults (target-app), route rows carry an app column and a file path in that app, one URL may exist once per app, and route evidence is scoped to the chosen app. - framework::App carries the package name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The devup-implementation-plan and devup skills and the OpenCode /devup command: view transitions are always on and proved by the runtime audit's view-transition check (a failure is a bug to fix); the update step says plainly that it updates every manifest, bun workspaces and Cargo included; the target app is picked by meaning. The guide, the tool descriptions and the README match, viewTransitions joins the config keys, and node_modules/ is ignored so a local harness install is never committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…checks The runtime audit executor now judges page health (error page text, page and console errors), size and position against Figma geometry, text cut off by its box, hover and focus states (one element or a sweep of every link and button) and element captures for visual comparison. View-transition checks move behind their screen's other checks when a plan is written, and the executor returns to the screen afterwards. Exports into a project mark the page root, its sections, their children and every asset with data-figma-node-id, and devup_visual_compare can write its diff PNG to a path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… autonomously devup_screen_plan action auditPlan writes one runtime-audit plan per confirmed URL: page health, overflow and clipped text at every width; the page, each section and each asset against their Figma renders at the drawn widths (viewports as tall as each frame); size and position of every section, card and asset; navigation containment; hover and focus sweeps; and a view-transition check to the next URL, placed last. Missing referencePngs, native renders and frames are reported with the export call that makes them. Ingest classifies every failure (runtime-error, overflow, layout-position, layout-size, asset-mismatch, section-visual, page-visual, interaction, view-transition) with its Figma node, code file and a fix hint, and returns ready asset and section comparisons with per-scope thresholds. status folds each run and the visual results into finding records, marks a finding unresolved with its evidence after maxFixAttempts (default 3), and returns a work queue per URL and the final report. defer records a decision as pending without stopping the work; assume takes it at the end, and a destructive one stays unbuilt. App selection now takes the public app whenever nothing says admin, asking only when no app looks public, and a mixed file's route options come from each screen's own app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The devup-implementation-plan and devup skills, the OpenCode /devup command, guide rule 16, the tool description and the README now carry auditPlan, the failure classes with their fix hints, visualResults, maxFixAttempts and unresolved findings, defer and pending decisions, the final report, the node-id attribute, the visual thresholds and the public-app bias, under three hard rules: visually verify every screen, section and asset at every drawn width; never stop to wait, reorder the work; done = zero fails, or listed as unresolved with evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…every page and route
Geometry at a drawn width is exact: bounds allow only half a pixel of
rounding (Chromium reports LayoutUnits, unsnapped at DPR 1, and the
converter rounds fractional Figma px) and take no tolerance. The audit plan
loads the frames' fonts first, so a missing font is an environment error.
An export into a project records what it wrote for every marked node in
.devup/exports/<node>.json. status compares a failing element's code with
that record: changed or added layout props are code-drift (restore the
export); code equal to the export is a converter-bug, written as a repro to
.devup/converter-bugs/<node>.json, reported in converterBugs and the final
report, and taken out of the fix loop.
Pages merged from one frame per width carry data-figma-node-id (the widest
width's node) and data-figma-node-ids ("<width>:<node>" per drawn width,
space-separated); the audit plan finds each section, card and asset per width
and checks elements a width does not draw are hidden (new hidden kind).
Dynamic routes are audited at concrete URLs from generateStaticParams
(literals or local data), else the links audited pages hold (collected by
the runner), else a sample from the detail frame, recorded as an assumption.
auditPlan puts a Vite plugin next to each app's vite.config that strips the
ids in vite build unless DEVUP_AUDIT=1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ic URLs and the production strip The skills, the OpenCode command, guide rule 16, the tool description and the README now say layout is exact (half a pixel of rounding, no tolerance) and a mismatch is code drift or a converter bug, describe the export record and the repro file, the data-figma-node-ids format, how dynamic routes get concrete URLs, and the Vite plugin that strips the ids unless DEVUP_AUDIT=1, with the build-output grep in the submission checklist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
owjs3901
marked this pull request as ready for review
October 17, 2026 17:44
owjs3901
deleted the
devfive/3099154426bffbeef8b62999b80bb038/devfive-relay-c70423/screen-plan-first
branch
October 17, 2026 17:44
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.
Summary
Two lines of work, combined into one PR at the reviewer's request.
1. Multi-colour icons stay
<Image>own_same_colortreated gradient/image/unreadable paints as "no colour", so{gradient, #552AFFBF, #552AFFBF}collapsed to one solid and became amaskImage. Unpainted layers were also order-sensitive. Both fixed;compat.rsnow masks only when every visible paint is the same solid.devup_ui_validate(withprojectRoot) reads eachmaskImageSVG and warns on gradients, patterns,<image>or more than one colour (ignoring<clipPath>/<mask>/<defs>).responseFields, and explain pendingfileState. Breaking:assetJobis now an alias reference only; readexportJob.2. Plan first, ask everything up front, then deliver a finished site
devup_screen_plan(draft | confirm | status | assume): maps every in-scope Figma screen to an evidence-backed URL. Unproven routes, width/section grouping, cross-section ambiguities, name-based guesses, exclusions (trash/archive/draft/copy/hidden) and site facts (favicon, OG image, site name/URL, analytics, motion) becomequestions/questionRoundsfor the runtime's question tool. All rounds are asked back to back before implementation. Afterconfirmthe agent never asks and recordsassumeentries instead..devup/screen-plan.config.jsoncan pre-answer: aliases, modal/exclude/include keywords, width grouping, favicon, ogImage, siteName, siteUrl, motion.needsDesignFix: they are reported, not built.devup_figma_exportgate: page writes need a confirmed plan. Excluded and design-fix frames are refused, and a route mismatch is refused._motionReduce;lang,altand not-found.devup_ui_validatesite checks: stateless controls, missing focus styles, too much motion, unlabeled icon buttons, pages without metadata, missing app icon.statusreports a URL × width table, site completeness, assumptions, design fixes and exclusions. Runtime audit ingest adds a per-screen, per-width table; an unaudited width is reported as not-run.devup-implementation-plananddevup, plus a/devupentry point: a Claude Code skill and an OpenCode command, installed bydevup_skills. The server instructions also start this workflow on any request to build pages or a site from Figma, even without/devup.Tests
cargo fmt --checkandcargo clippy --workspace --all-targets -- -D warningsare clean.cargo test --workspacepasses except two symlink tests that fail with Windows os error 1314. They fail the same way onmain.composite_export::r3_asset_job_timeout_keeps_first_asset_and_resumes_pending_readis timing-sensitive: it failed once in a full run and passed on re-run.Review notes
viewTransitionflag path. Only React 19.3 is documented as needing no flag.all 0.2s ease).Submitted via Devfive hub. Employee: ; device:
3099154426bffbeef8b62999b80bb038